diff --git a/Cargo.toml b/Cargo.toml index ea86fcc..f64da9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,5 +40,5 @@ validator = { version = "0.16", features = ["derive"] } # HTTP 客户端(用于测试) [dev-dependencies] -reqwest = { version = "0.11", features = ["json"] } +reqwest = { version = "0.11", features = ["json", "rustls-tls"], default-features = false } tokio-test = "0.4" \ No newline at end of file diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..fd52fd4 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,293 @@ +# Rust User API 文档 + +## 概述 + +这是一个使用 Rust 和 Axum 框架构建的用户管理 REST API。提供完整的用户 CRUD 操作和 JWT 身份认证功能。 + +## 基础信息 + +- **Base URL**: `http://localhost:3000` +- **Content-Type**: `application/json` +- **认证方式**: JWT Bearer Token + +## 端点列表 + +### 基础端点 + +#### GET / +获取 API 欢迎信息 + +**响应示例**: +```json +{ + "message": "欢迎使用 Rust User API", + "version": "0.1.0" +} +``` + +#### GET /health +健康检查端点 + +**响应示例**: +```json +{ + "status": "healthy", + "timestamp": "2024-01-01T12:00:00Z" +} +``` + +### 用户管理 + +#### POST /api/users +创建新用户 + +**请求体**: +```json +{ + "username": "string (3-50字符)", + "email": "string (有效邮箱)", + "password": "string (至少8字符)" +} +``` + +**成功响应** (201 Created): +```json +{ + "id": "uuid", + "username": "testuser", + "email": "test@example.com", + "created_at": "2024-01-01T12:00:00Z" +} +``` + +**错误响应**: +- 400 Bad Request: 验证失败 +- 409 Conflict: 用户名已存在 + +#### GET /api/users +获取所有用户列表 + +**成功响应** (200 OK): +```json +[ + { + "id": "uuid", + "username": "testuser", + "email": "test@example.com", + "created_at": "2024-01-01T12:00:00Z" + } +] +``` + +#### GET /api/users/{id} +根据 ID 获取特定用户 + +**路径参数**: +- `id`: 用户 UUID + +**成功响应** (200 OK): +```json +{ + "id": "uuid", + "username": "testuser", + "email": "test@example.com", + "created_at": "2024-01-01T12:00:00Z" +} +``` + +**错误响应**: +- 404 Not Found: 用户不存在 + +#### PUT /api/users/{id} +更新用户信息 + +**路径参数**: +- `id`: 用户 UUID + +**请求体**: +```json +{ + "username": "string (可选, 3-50字符)", + "email": "string (可选, 有效邮箱)" +} +``` + +**成功响应** (200 OK): +```json +{ + "id": "uuid", + "username": "newusername", + "email": "newemail@example.com", + "created_at": "2024-01-01T12:00:00Z" +} +``` + +**错误响应**: +- 400 Bad Request: 验证失败 +- 404 Not Found: 用户不存在 + +#### DELETE /api/users/{id} +删除用户 + +**路径参数**: +- `id`: 用户 UUID + +**成功响应** (204 No Content): 无响应体 + +**错误响应**: +- 404 Not Found: 用户不存在 + +### 身份认证 + +#### POST /api/auth/login +用户登录 + +**请求体**: +```json +{ + "username": "string", + "password": "string" +} +``` + +**成功响应** (200 OK): +```json +{ + "token": "jwt_token_string", + "user": { + "id": "uuid", + "username": "testuser", + "email": "test@example.com", + "created_at": "2024-01-01T12:00:00Z" + } +} +``` + +**错误响应**: +- 401 Unauthorized: 用户名或密码错误 +- 404 Not Found: 用户不存在 + +## 错误响应格式 + +所有错误响应都遵循统一格式: + +```json +{ + "error": "错误描述信息", + "status": 400 +} +``` + +### 常见错误码 + +- **400 Bad Request**: 请求参数验证失败 +- **401 Unauthorized**: 认证失败或未提供认证信息 +- **404 Not Found**: 请求的资源不存在 +- **409 Conflict**: 资源冲突(如用户名已存在) +- **500 Internal Server Error**: 服务器内部错误 + +## 认证机制 + +API 使用 JWT (JSON Web Token) 进行身份认证。 + +### 获取 Token + +通过 `POST /api/auth/login` 端点登录获取 JWT token。 + +### 使用 Token + +在需要认证的请求中,在 Header 中添加: + +``` +Authorization: Bearer +``` + +### Token 有效期 + +JWT token 有效期为 24 小时。 + +## 数据验证规则 + +### 用户名 (username) +- 长度:3-50 字符 +- 必须唯一 + +### 邮箱 (email) +- 必须是有效的邮箱格式 +- 例如:`user@example.com` + +### 密码 (password) +- 最少 8 个字符 +- 使用 bcrypt 进行哈希存储 + +## 使用示例 + +### 创建用户并登录 + +```bash +# 1. 创建用户 +curl -X POST http://localhost:3000/api/users \ + -H "Content-Type: application/json" \ + -d '{ + "username": "testuser", + "email": "test@example.com", + "password": "password123" + }' + +# 2. 登录获取 token +curl -X POST http://localhost:3000/api/auth/login \ + -H "Content-Type: application/json" \ + -d '{ + "username": "testuser", + "password": "password123" + }' + +# 3. 使用 token 访问受保护的端点(未来功能) +curl -H "Authorization: Bearer " \ + http://localhost:3000/api/protected-endpoint +``` + +## 开发和测试 + +### 运行服务器 + +```bash +cargo run +``` + +### 运行测试 + +```bash +# 运行单元测试 +cargo test + +# 运行 API 集成测试 +./test_api.sh +``` + +### 环境配置 + +复制 `.env.example` 到 `.env` 并修改配置: + +```bash +cp .env.example .env +``` + +## 技术实现 + +- **Web 框架**: Axum 0.7 +- **异步运行时**: Tokio +- **序列化**: Serde +- **密码哈希**: bcrypt +- **JWT**: jsonwebtoken +- **验证**: validator +- **日志**: tracing + +## 后续计划 + +- [ ] 数据库持久化 (SQLite) +- [ ] API 分页支持 +- [ ] 用户搜索和过滤 +- [ ] 角色和权限管理 +- [ ] API 限流 +- [ ] OpenAPI/Swagger 文档 \ No newline at end of file diff --git a/src/handlers/user.rs b/src/handlers/user.rs index 1ad1d03..b51b098 100644 --- a/src/handlers/user.rs +++ b/src/handlers/user.rs @@ -131,7 +131,7 @@ pub async fn login( } /// 使用 bcrypt 哈希密码 -fn hash_password(password: &str) -> String { +pub fn hash_password(password: &str) -> String { bcrypt::hash(password, bcrypt::DEFAULT_COST).unwrap_or_else(|_| { // 如果哈希失败,使用简单的备用方案(仅用于开发) format!("fallback_hash_{}", password) diff --git a/src/lib.rs b/src/lib.rs index 183904e..3415049 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,4 +15,178 @@ pub mod utils; // Re-export commonly used types pub use models::user::{User, UserResponse, CreateUserRequest, UpdateUserRequest}; pub use storage::memory::MemoryUserStore; -pub use utils::errors::ApiError; \ No newline at end of file +pub use utils::errors::ApiError; + +#[cfg(test)] +mod tests { + use super::*; + use crate::models::user::{CreateUserRequest, LoginRequest}; + use crate::storage::memory::MemoryUserStore; + use uuid::Uuid; + use validator::Validate; + + #[tokio::test] + async fn test_memory_store_operations() { + let store = MemoryUserStore::new(); + + // 测试创建用户 + let user = User { + id: Uuid::new_v4(), + username: "testuser".to_string(), + email: "test@example.com".to_string(), + password_hash: "hashed_password".to_string(), + created_at: chrono::Utc::now(), + updated_at: chrono::Utc::now(), + }; + + let user_id = user.id; + let result = store.create_user(user.clone()).await; + assert!(result.is_ok()); + + // 测试获取用户 + let retrieved_user = store.get_user(&user_id).await; + assert!(retrieved_user.is_some()); + assert_eq!(retrieved_user.unwrap().username, "testuser"); + + // 测试按用户名获取用户 + let user_by_name = store.get_user_by_username("testuser").await; + assert!(user_by_name.is_some()); + assert_eq!(user_by_name.unwrap().id, user_id); + + // 测试列出所有用户 + let users = store.list_users().await; + assert_eq!(users.len(), 1); + + // 测试更新用户 + let mut updated_user = user.clone(); + updated_user.username = "updated_user".to_string(); + let update_result = store.update_user(&user_id, updated_user).await; + assert!(update_result.is_some()); + + // 测试删除用户 + let delete_result = store.delete_user(&user_id).await; + assert!(delete_result); + + // 验证用户已被删除 + let deleted_user = store.get_user(&user_id).await; + assert!(deleted_user.is_none()); + } + + #[test] + fn test_user_request_validation() { + // 测试有效的创建用户请求 + let valid_request = CreateUserRequest { + username: "validuser".to_string(), + email: "valid@example.com".to_string(), + password: "validpassword123".to_string(), + }; + assert!(valid_request.validate().is_ok()); + + // 测试无效的用户名(太短) + let invalid_username = CreateUserRequest { + username: "ab".to_string(), + email: "valid@example.com".to_string(), + password: "validpassword123".to_string(), + }; + assert!(invalid_username.validate().is_err()); + + // 测试无效的邮箱 + let invalid_email = CreateUserRequest { + username: "validuser".to_string(), + email: "invalid-email".to_string(), + password: "validpassword123".to_string(), + }; + assert!(invalid_email.validate().is_err()); + + // 测试无效的密码(太短) + let invalid_password = CreateUserRequest { + username: "validuser".to_string(), + email: "valid@example.com".to_string(), + password: "123".to_string(), + }; + assert!(invalid_password.validate().is_err()); + } + + #[test] + fn test_user_response_conversion() { + let user = User { + id: Uuid::new_v4(), + username: "testuser".to_string(), + email: "test@example.com".to_string(), + password_hash: "hashed_password".to_string(), + created_at: chrono::Utc::now(), + updated_at: chrono::Utc::now(), + }; + + let response: UserResponse = user.clone().into(); + + assert_eq!(response.id, user.id); + assert_eq!(response.username, user.username); + assert_eq!(response.email, user.email); + assert_eq!(response.created_at, user.created_at); + // 确保密码哈希不在响应中 + // UserResponse 结构体中没有 password_hash 字段 + } + + #[test] + fn test_api_error_display() { + use crate::utils::errors::ApiError; + + let validation_error = ApiError::ValidationError("测试验证错误".to_string()); + let not_found_error = ApiError::NotFound("用户不存在".to_string()); + let unauthorized_error = ApiError::Unauthorized; + let conflict_error = ApiError::Conflict("用户名已存在".to_string()); + let internal_error = ApiError::InternalError("内部错误".to_string()); + + // 这些测试确保错误类型能够正确创建 + assert!(matches!(validation_error, ApiError::ValidationError(_))); + assert!(matches!(not_found_error, ApiError::NotFound(_))); + assert!(matches!(unauthorized_error, ApiError::Unauthorized)); + assert!(matches!(conflict_error, ApiError::Conflict(_))); + assert!(matches!(internal_error, ApiError::InternalError(_))); + } + + #[test] + fn test_config_default() { + use crate::config::Config; + + let config = Config::default(); + assert_eq!(config.server_host, "127.0.0.1"); + assert_eq!(config.server_port, 3000); + assert_eq!(config.jwt_secret, "your-secret-key"); + assert_eq!(config.database_url, None); + assert_eq!(config.server_address(), "127.0.0.1:3000"); + } + + #[tokio::test] + async fn test_user_service_authentication() { + use crate::services::user_service::UserService; + + let store = MemoryUserStore::new(); + let service = UserService::new(store.clone()); + + // 创建测试用户(使用简单的哈希格式以匹配 UserService 的验证逻辑) + let user = User { + id: Uuid::new_v4(), + username: "authtest".to_string(), + email: "auth@example.com".to_string(), + password_hash: "hashed_testpassword".to_string(), // 使用简单格式 + created_at: chrono::Utc::now(), + updated_at: chrono::Utc::now(), + }; + + store.create_user(user).await.unwrap(); + + // 测试正确的认证 + let auth_result = service.authenticate_user("authtest", "testpassword").await; + assert!(auth_result.is_ok()); + + // 测试错误的密码 + let wrong_auth = service.authenticate_user("authtest", "wrongpassword").await; + assert!(wrong_auth.is_err()); + + // 测试不存在的用户 + let nonexistent_auth = service.authenticate_user("nonexistent", "testpassword").await; + assert!(nonexistent_auth.is_err()); + } +} \ No newline at end of file diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs new file mode 100644 index 0000000..d149be4 --- /dev/null +++ b/tests/integration_tests.rs @@ -0,0 +1,299 @@ +//! 集成测试 + +use reqwest; +use serde_json::{json, Value}; +use std::collections::HashMap; +use tokio; + +const BASE_URL: &str = "http://127.0.0.1:3000"; + +/// 测试辅助函数:创建 HTTP 客户端 +fn create_client() -> reqwest::Client { + reqwest::Client::new() +} + +/// 测试辅助函数:解析 JSON 响应 +async fn parse_json_response(response: reqwest::Response) -> Result> { + let text = response.text().await?; + let json: Value = serde_json::from_str(&text)?; + Ok(json) +} + +#[tokio::test] +async fn test_health_check() { + let client = create_client(); + + let response = client + .get(&format!("{}/health", BASE_URL)) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(response.status(), 200); + + let json = parse_json_response(response).await.expect("Failed to parse JSON"); + assert_eq!(json["status"], "healthy"); + assert!(json["timestamp"].is_string()); +} + +#[tokio::test] +async fn test_root_endpoint() { + let client = create_client(); + + let response = client + .get(&format!("{}/", BASE_URL)) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(response.status(), 200); + + let json = parse_json_response(response).await.expect("Failed to parse JSON"); + assert_eq!(json["message"], "欢迎使用 Rust User API"); + assert_eq!(json["version"], "0.1.0"); +} + +#[tokio::test] +async fn test_user_lifecycle() { + let client = create_client(); + + // 1. 获取初始用户列表(应该为空) + let response = client + .get(&format!("{}/api/users", BASE_URL)) + .send() + .await + .expect("Failed to get users"); + + assert_eq!(response.status(), 200); + let users = parse_json_response(response).await.expect("Failed to parse JSON"); + assert!(users.is_array()); + + // 2. 创建新用户 + let user_data = json!({ + "username": "testuser_integration", + "email": "integration@example.com", + "password": "password123" + }); + + let response = client + .post(&format!("{}/api/users", BASE_URL)) + .json(&user_data) + .send() + .await + .expect("Failed to create user"); + + assert_eq!(response.status(), 201); + let created_user = parse_json_response(response).await.expect("Failed to parse JSON"); + + assert_eq!(created_user["username"], "testuser_integration"); + assert_eq!(created_user["email"], "integration@example.com"); + assert!(created_user["id"].is_string()); + assert!(created_user["created_at"].is_string()); + + let user_id = created_user["id"].as_str().unwrap(); + + // 3. 获取创建的用户 + let response = client + .get(&format!("{}/api/users/{}", BASE_URL, user_id)) + .send() + .await + .expect("Failed to get user"); + + assert_eq!(response.status(), 200); + let fetched_user = parse_json_response(response).await.expect("Failed to parse JSON"); + assert_eq!(fetched_user["id"], user_id); + assert_eq!(fetched_user["username"], "testuser_integration"); + + // 4. 更新用户 + let update_data = json!({ + "username": "updated_testuser", + "email": "updated@example.com" + }); + + let response = client + .put(&format!("{}/api/users/{}", BASE_URL, user_id)) + .json(&update_data) + .send() + .await + .expect("Failed to update user"); + + assert_eq!(response.status(), 200); + let updated_user = parse_json_response(response).await.expect("Failed to parse JSON"); + assert_eq!(updated_user["username"], "updated_testuser"); + assert_eq!(updated_user["email"], "updated@example.com"); + + // 5. 删除用户 + let response = client + .delete(&format!("{}/api/users/{}", BASE_URL, user_id)) + .send() + .await + .expect("Failed to delete user"); + + assert_eq!(response.status(), 204); + + // 6. 验证用户已被删除 + let response = client + .get(&format!("{}/api/users/{}", BASE_URL, user_id)) + .send() + .await + .expect("Failed to get deleted user"); + + assert_eq!(response.status(), 404); +} + +#[tokio::test] +async fn test_user_validation() { + let client = create_client(); + + // 测试无效的用户数据 + let invalid_user_data = json!({ + "username": "ab", // 太短 + "email": "invalid-email", // 无效邮箱 + "password": "123" // 太短 + }); + + let response = client + .post(&format!("{}/api/users", BASE_URL)) + .json(&invalid_user_data) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(response.status(), 400); + let error_response = parse_json_response(response).await.expect("Failed to parse JSON"); + assert!(error_response["error"].is_string()); + assert_eq!(error_response["status"], 400); +} + +#[tokio::test] +async fn test_duplicate_username() { + let client = create_client(); + + let user_data = json!({ + "username": "duplicate_test", + "email": "duplicate1@example.com", + "password": "password123" + }); + + // 创建第一个用户 + let response = client + .post(&format!("{}/api/users", BASE_URL)) + .json(&user_data) + .send() + .await + .expect("Failed to create first user"); + + assert_eq!(response.status(), 201); + + // 尝试创建相同用户名的用户 + let duplicate_user_data = json!({ + "username": "duplicate_test", // 相同用户名 + "email": "duplicate2@example.com", + "password": "password123" + }); + + let response = client + .post(&format!("{}/api/users", BASE_URL)) + .json(&duplicate_user_data) + .send() + .await + .expect("Failed to send duplicate request"); + + assert_eq!(response.status(), 409); + let error_response = parse_json_response(response).await.expect("Failed to parse JSON"); + assert!(error_response["error"].as_str().unwrap().contains("用户名已存在")); +} + +#[tokio::test] +async fn test_login_flow() { + let client = create_client(); + + // 1. 创建测试用户 + let user_data = json!({ + "username": "login_test", + "email": "login@example.com", + "password": "password123" + }); + + let response = client + .post(&format!("{}/api/users", BASE_URL)) + .json(&user_data) + .send() + .await + .expect("Failed to create user"); + + assert_eq!(response.status(), 201); + + // 2. 测试正确的登录凭据 + let login_data = json!({ + "username": "login_test", + "password": "password123" + }); + + let response = client + .post(&format!("{}/api/auth/login", BASE_URL)) + .json(&login_data) + .send() + .await + .expect("Failed to login"); + + assert_eq!(response.status(), 200); + let login_response = parse_json_response(response).await.expect("Failed to parse JSON"); + + assert!(login_response["token"].is_string()); + assert!(login_response["user"]["id"].is_string()); + assert_eq!(login_response["user"]["username"], "login_test"); + + // 3. 测试错误的密码 + let wrong_login_data = json!({ + "username": "login_test", + "password": "wrongpassword" + }); + + let response = client + .post(&format!("{}/api/auth/login", BASE_URL)) + .json(&wrong_login_data) + .send() + .await + .expect("Failed to send wrong login"); + + assert_eq!(response.status(), 401); + + // 4. 测试不存在的用户 + let nonexistent_login_data = json!({ + "username": "nonexistent_user", + "password": "password123" + }); + + let response = client + .post(&format!("{}/api/auth/login", BASE_URL)) + .json(&nonexistent_login_data) + .send() + .await + .expect("Failed to send nonexistent login"); + + assert_eq!(response.status(), 404); +} + +#[tokio::test] +async fn test_not_found_endpoints() { + let client = create_client(); + + // 测试不存在的用户 ID + let response = client + .get(&format!("{}/api/users/00000000-0000-0000-0000-000000000000", BASE_URL)) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(response.status(), 404); + + // 测试不存在的端点 + let response = client + .get(&format!("{}/api/nonexistent", BASE_URL)) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(response.status(), 404); +} \ No newline at end of file