156 lines
5.1 KiB
Rust
156 lines
5.1 KiB
Rust
use axum::{Router, body::Body, routing::get};
|
||
use common_telemetry::{
|
||
error::AppError,
|
||
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<String, AppError> {
|
||
// 模拟业务逻辑判断...
|
||
Err(AppError::AccessTokenExpired)
|
||
}
|
||
|
||
// 模拟场景2:Refresh Token 过期
|
||
async fn handler_refresh_expired() -> Result<String, AppError> {
|
||
Err(AppError::RefreshTokenExpired)
|
||
}
|
||
|
||
// 模拟场景3:正常成功
|
||
async fn handler_success() -> Result<String, AppError> {
|
||
// 这里打印一条日志,测试 Tracing 是否工作
|
||
tracing::info!(user_id = 10086, "User accessed success handler");
|
||
Ok("Success Data".to_string())
|
||
}
|
||
|
||
#[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));
|
||
|
||
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();
|
||
|
||
// 稍微等待异步日志写入磁盘
|
||
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 会在作用域结束时自动删除清理
|
||
}
|