299 lines
9.6 KiB
Rust
299 lines
9.6 KiB
Rust
use axum::{Router, body::Body, routing::get};
|
||
use common_telemetry::{
|
||
error::AppError,
|
||
response::AppResponse,
|
||
axum_middleware::trace_http_request,
|
||
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())
|
||
}
|
||
|
||
async fn handler_success_wrapped() -> Result<AppResponse<String>, AppError> {
|
||
Ok(AppResponse::ok("Success Data".to_string()))
|
||
}
|
||
|
||
async fn handler_bad_request() -> Result<String, AppError> {
|
||
Err(AppError::BadRequest("bad params".into()))
|
||
}
|
||
|
||
#[cfg(feature = "with-validator")]
|
||
async fn handler_validation_error() -> Result<String, AppError> {
|
||
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 会在作用域结束时自动删除清理
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn error_log_includes_http_context_span() {
|
||
use std::sync::{Arc, Mutex};
|
||
use tracing_subscriber::fmt::MakeWriter;
|
||
|
||
struct BufferWriter(Arc<Mutex<Vec<u8>>>);
|
||
impl<'a> MakeWriter<'a> for BufferWriter {
|
||
type Writer = BufferGuard;
|
||
fn make_writer(&'a self) -> Self::Writer {
|
||
BufferGuard(self.0.clone())
|
||
}
|
||
}
|
||
struct BufferGuard(Arc<Mutex<Vec<u8>>>);
|
||
impl std::io::Write for BufferGuard {
|
||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||
self.0.lock().unwrap().extend_from_slice(buf);
|
||
Ok(buf.len())
|
||
}
|
||
fn flush(&mut self) -> std::io::Result<()> {
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
async fn handler() -> Result<String, AppError> {
|
||
Err(AppError::MissingAuthHeader)
|
||
}
|
||
|
||
let buf = Arc::new(Mutex::new(Vec::<u8>::new()));
|
||
let subscriber = tracing_subscriber::fmt()
|
||
.with_writer(BufferWriter(buf.clone()))
|
||
.with_ansi(false)
|
||
.json()
|
||
.with_current_span(true)
|
||
.with_span_list(true)
|
||
.finish();
|
||
let _guard = tracing::subscriber::set_default(subscriber);
|
||
|
||
let app = Router::new()
|
||
.route("/needs-auth", get(handler))
|
||
.layer(axum::middleware::from_fn(trace_http_request));
|
||
|
||
let resp = app
|
||
.oneshot(
|
||
Request::builder()
|
||
.uri("/needs-auth")
|
||
.body(Body::empty())
|
||
.unwrap(),
|
||
)
|
||
.await
|
||
.unwrap();
|
||
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
|
||
|
||
let logs = String::from_utf8(buf.lock().unwrap().clone()).unwrap();
|
||
assert!(logs.contains("\"message\":\"Request failed\""));
|
||
assert!(logs.contains("/needs-auth"));
|
||
assert!(logs.contains("http.method") || logs.contains("GET"));
|
||
}
|