use axum::{Router, body::Body, routing::get}; use common_telemetry::{ error::AppError, response::AppResponse, telemetry::{self, TelemetryConfig}, }; use http::{Request, StatusCode}; use serde_json::Value; use std::{fs, time::Duration}; use tower::ServiceExt; // for oneshot // --- 模拟业务 Handler --- // 模拟场景1:Access Token 过期 async fn handler_access_expired() -> Result { // 模拟业务逻辑判断... Err(AppError::AccessTokenExpired) } // 模拟场景2:Refresh Token 过期 async fn handler_refresh_expired() -> Result { Err(AppError::RefreshTokenExpired) } // 模拟场景3:正常成功 async fn handler_success() -> Result { // 这里打印一条日志,测试 Tracing 是否工作 tracing::info!(user_id = 10086, "User accessed success handler"); Ok("Success Data".to_string()) } async fn handler_success_wrapped() -> Result, AppError> { Ok(AppResponse::ok("Success Data".to_string())) } async fn handler_bad_request() -> Result { Err(AppError::BadRequest("bad params".into())) } #[cfg(feature = "with-validator")] async fn handler_validation_error() -> Result { Err(AppError::ValidationError("field required".into())) } #[tokio::test] async fn test_full_flow_error_and_logging() { println!(">>> [Step 0] Test Initializing..."); // 1. 准备环境:创建一个临时目录存放日志,防止污染项目 let temp_dir = tempfile::tempdir().unwrap(); let log_dir_path = temp_dir.path().to_str().unwrap().to_string(); let log_filename = "test_app.log"; // 2. 初始化 Tracing (模拟生产环境配置) let config = TelemetryConfig { service_name: "test-service".into(), log_level: "info".into(), log_to_file: true, // 开启文件写入 log_dir: Some(log_dir_path.clone()), log_file: Some(log_filename.to_string()), }; // !!! 必须持有 guard,否则日志不会写入 let _guard = telemetry::init(config); // 给一点时间让 tracing 系统初始化 tokio::time::sleep(Duration::from_millis(50)).await; // 3. 构建 Axum Router let app = Router::new() .route("/access-expired", get(handler_access_expired)) .route("/refresh-expired", get(handler_refresh_expired)) .route("/success", get(handler_success)) .route("/success-wrapped", get(handler_success_wrapped)) .route("/bad-request", get(handler_bad_request)); #[cfg(feature = "with-validator")] let app = app.route("/validation-error", get(handler_validation_error)); println!(">>> [Step 1] Running Case A: Access Token Expired..."); // --- 测试用例 A: Access Token 过期 --- let response = app .clone() .oneshot( Request::builder() .uri("/access-expired") .body(Body::empty()) .unwrap(), ) .await .unwrap(); // 验证 HTTP 状态码 (401) assert_eq!(response.status(), StatusCode::UNAUTHORIZED); // 验证 JSON Body let body_bytes = axum::body::to_bytes(response.into_body(), usize::MAX) .await .unwrap(); let body: Value = serde_json::from_slice(&body_bytes).unwrap(); // 核心验证:code 必须是 20001 (BizCode::AccessTokenExpired) // 前端根据这个 code 进行静默刷新 assert_eq!(body["code"], 20001); // --- 测试用例 B: Refresh Token 过期 --- println!(">>> [Step 2] Running Case B: Refresh Token Expired..."); let response = app .clone() .oneshot( Request::builder() .uri("/refresh-expired") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); let body_bytes = axum::body::to_bytes(response.into_body(), usize::MAX) .await .unwrap(); let body: Value = serde_json::from_slice(&body_bytes).unwrap(); // 核心验证:code 必须是 20002 (BizCode::RefreshTokenExpired) // 前端根据这个 code 强制登出 assert_eq!(body["code"], 20002); // --- 测试用例 C: 验证日志文件是否生成 --- println!(">>> [Step 3] Running Case C: Log File Verification..."); // 先请求一次成功接口,触发 tracing::info! let _ = app .clone() .oneshot( Request::builder() .uri("/success") .body(Body::empty()) .unwrap(), ) .await .unwrap(); // --- 测试用例 D: 统一成功响应格式 --- println!(">>> [Step 4] Running Case D: Success Response Wrapper..."); let response = app .clone() .oneshot( Request::builder() .uri("/success-wrapped") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let body_bytes = axum::body::to_bytes(response.into_body(), usize::MAX) .await .unwrap(); let body: Value = serde_json::from_slice(&body_bytes).unwrap(); assert_eq!(body["code"], 0); assert_eq!(body["message"], "Success"); assert_eq!(body["data"], "Success Data"); // --- 测试用例 E: 错误响应 details 字段 --- println!(">>> [Step 5] Running Case E: Error Response Details..."); let response = app .clone() .oneshot( Request::builder() .uri("/bad-request") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::BAD_REQUEST); let body_bytes = axum::body::to_bytes(response.into_body(), usize::MAX) .await .unwrap(); let body: Value = serde_json::from_slice(&body_bytes).unwrap(); assert_eq!(body["code"], 30000); assert_eq!(body["details"], "bad params"); #[cfg(feature = "with-validator")] { let response = app .clone() .oneshot( Request::builder() .uri("/validation-error") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::BAD_REQUEST); let body_bytes = axum::body::to_bytes(response.into_body(), usize::MAX) .await .unwrap(); let body: Value = serde_json::from_slice(&body_bytes).unwrap(); assert_eq!(body["code"], 30001); assert_eq!(body["details"], "field required"); } // 稍微等待异步日志写入磁盘 println!(" Waiting for logs to flush..."); tokio::time::sleep(Duration::from_millis(200)).await; // 验证文件 let mut found_file = false; let paths = fs::read_dir(temp_dir.path()).unwrap(); for path in paths { let entry = path.unwrap(); let file_name = entry.file_name().into_string().unwrap(); println!(" Found file in temp dir: {}", file_name); // tracing-appender 滚动日志通常格式为: filename.YYYY-MM-DD if file_name.starts_with(log_filename) { found_file = true; let content = fs::read_to_string(entry.path()).unwrap(); println!(" Log Content Preview: {}", content.trim()); assert!( content.contains("User accessed success handler"), "日志内容丢失!" ); assert!(content.contains("\"user_id\":10086"), "结构化字段丢失!"); } } assert!(found_file, "未在临时目录找到日志文件!"); println!(" [Success] Case C Passed (Log file verified)"); // temp_dir 会在作用域结束时自动删除清理 }