feat(project): init

This commit is contained in:
2026-01-29 14:37:31 +08:00
commit 88868050ba
8 changed files with 627 additions and 0 deletions

155
tests/integration_test.rs Normal file
View File

@@ -0,0 +1,155 @@
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 ---
// 模拟场景1Access Token 过期
async fn handler_access_expired() -> Result<String, AppError> {
// 模拟业务逻辑判断...
Err(AppError::AccessTokenExpired)
}
// 模拟场景2Refresh 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 会在作用域结束时自动删除清理
}