Skip to main content

sz_rust_core/
controller.rs

1//! 控制器模块 — SzController trait
2//!
3//! 对齐 PHP `app\SzController`(abstract class extends BaseController)。
4//! 继承链:BaseController → SzController → AddonsBaseController → 业务控制器。
5//!
6//! ## PHP 对齐
7//!
8//! | PHP 方法 | 签名 | Rust 等价 |
9//! |---------|------|-----------|
10//! | `renderJson($code=1, $msg='', $data=[])` | 返回 `compact('code','msg','data')` 数组 | [`SzController::render_json`] 返回 `Value::Object` |
11//! | `renderSuccess($msg='success', $data=[])` | `json(renderJson(1, $msg, $data))` | [`SzController::render_success`] 返回 `Response` |
12//! | `renderError($msg='error', $data=[], $code=0)` | `json(renderJson($code, $msg, $data))` | [`SzController::render_error`] 返回 `Response` |
13//! | `postData($key=null)` | `$this->request->param(...)` | [`SzController::post_data`] / [`SzController::post_data_by_key`] |
14//! | `getData($key=null)` | `$this->request->get(...)` | [`SzController::get_data`] / [`SzController::get_data_by_key`] |
15//!
16//! ## 参数顺序差异
17//!
18//! PHP `renderSuccess($msg, $data)` 与 Rust `ApiResponse::success(data, msg)` 参数顺序相反。
19//! 本 trait 严格遵循 PHP 顺序(msg 在前),内部调用 `ApiResponse` 时调换参数。
20//!
21//! ## Request 传递
22//!
23//! PHP 通过 `$this->request` 在控制器内部访问当前请求;Rust 控制器无状态,
24//! 由 handler 在每次请求时将 `Request<Body>` 作为参数传入 trait 方法。
25//!
26//! ## async trait
27//!
28//! 使用 Rust 1.75+ 原生 `async fn` in trait。trait 暂不支持 `dyn SzController`;
29//! 若未来需要 trait object,可改用 `#[async_trait]` 宏(届时增加 `async-trait` 依赖)。
30
31use axum::body::Body;
32use axum::http::Request;
33use axum::response::{IntoResponse, Response};
34use indexmap::IndexMap;
35use once_cell::sync::Lazy;
36use serde_json::{Map, Value};
37use std::future::Future;
38
39use crate::request::{
40    fetch_post_data, fetch_post_data_by_key, fetch_query_data, fetch_query_data_by_key,
41};
42use crate::response::ApiResponse;
43use crate::validate::Validate;
44
45// ============================================================================
46// JWT 配置(运行时从环境变量读取,对齐 PHP Token 类私有 $_config)
47// ============================================================================
48//
49// PHP `app\common\service\jwt\Token::$_config` 包含 issuer/audience/id/sign/expire,
50// 这些敏感信息不应硬编码到框架中。Rust 实现通过环境变量注入:
51//
52// - `SZ_JWT_SECRET`:签名密钥(PHP 实际使用 `id` 字段作为 HMAC 密钥,而非 `sign`)
53// - `SZ_JWT_ISSUER`:签发人(对应 PHP `issuer`,如 `https://mall.ljclz.shop`)
54//
55// 注:sz-orm-auth 的 JwtClaims 暂不包含 `aud` 字段,因此 audience 验证由
56// 业务层在解码后自行实现(PHP `PermittedFor` 约束等价)。
57
58/// JWT 配置(运行时从环境变量读取)
59///
60/// ## 安全约束
61///
62/// - `JwtConfig` 未派生 `Serialize`/`Deserialize`,密钥绝不会出现在序列化输出中。
63/// - `Debug` 手动实现:`secret` 字段始终脱敏为 `"[REDACTED]"`,
64///   防止 `{:?}` 格式化时将密钥泄漏到日志或 panic 信息中(P1-SEC-12)。
65/// - 未来若需派生 `Serialize`,必须为 `secret` 字段添加
66///   `#[serde(skip_serializing)]` 防止日志/响应泄露。
67#[derive(Clone, Default)]
68struct JwtConfig {
69    /// 签名密钥(对应 PHP `$_config['id']`,Lcobucci 用作 HMAC 密钥)
70    secret: String,
71    /// 签发人(对应 PHP `$_config['issuer']`)
72    issuer: String,
73    /// 接收人(对应 PHP `$_config['permitted_for']`)— P1-SEC-10 新增
74    ///
75    /// 为空时跳过 aud 验证(向后兼容旧 token);非空时要求 token 的 `aud` 字段匹配。
76    audience: String,
77}
78
79impl std::fmt::Debug for JwtConfig {
80    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81        f.debug_struct("JwtConfig")
82            .field("secret", &"[REDACTED]")
83            .field("issuer", &self.issuer)
84            .field("audience", &self.audience)
85            .finish()
86    }
87}
88
89/// 全局 JWT 配置实例(启动时从环境变量读取一次)
90///
91/// - `SZ_JWT_SECRET`:签名密钥(**必填且不可为空**,未设置或为空时 panic,阻止服务启动)
92/// - `SZ_JWT_ISSUER`:签发人(可选,空字符串表示跳过 iss 验证)
93///
94/// 安全铁律(P0-SEC-02):密钥为空时静默回退等同于禁用认证,攻击者可伪造任意 token。
95/// 此处与 `AuthConfig` 的 `missing_key => panic!` 行为保持一致,
96/// 统一为"密钥缺失即拒绝启动",消除两套 JWT 配置的行为不一致风险。
97static JWT_CONFIG: Lazy<Option<JwtConfig>> = Lazy::new(|| {
98    let secret = std::env::var("SZ_JWT_SECRET").ok()?;
99    // 空字符串视为未配置(与安全铁律一致:空密钥 = 无密钥)
100    if secret.is_empty() {
101        return None;
102    }
103    Some(JwtConfig {
104        secret,
105        issuer: std::env::var("SZ_JWT_ISSUER").unwrap_or_default(),
106        // P1-SEC-10:从环境变量读取 audience,未设置时为空(跳过 aud 验证)
107        audience: std::env::var("SZ_JWT_AUDIENCE").unwrap_or_default(),
108    })
109});
110
111/// 启动时校验 JWT 配置(生产环境必须在 `main()` 中调用)
112///
113/// 若 `SZ_JWT_SECRET` 未设置则 panic,阻止服务启动,防止认证形同虚设。
114/// 测试环境中可跳过此校验(`JWT_CONFIG` 为 `None` 时 `get_token` 返回 `Ok(None)`)。
115pub fn validate_jwt_config() {
116    if JWT_CONFIG.is_none() {
117        panic!("SZ_JWT_SECRET 环境变量未设置 — 生产环境必须通过环境变量提供 JWT 密钥");
118    }
119}
120
121/// 去除 Authorization header 中的 Bearer 前缀
122///
123/// 对齐 PHP `str_ireplace('bearer', '', $header)`:大小写不敏感地移除 `bearer` 标识。
124/// 支持以下格式(与 PHP 兼容):
125/// - `Bearer xxx.yyy.zzz` → `xxx.yyy.zzz`
126/// - `bearer xxx.yyy.zzz` → `xxx.yyy.zzz`
127/// - `BEARER xxx.yyy.zzz` → `xxx.yyy.zzz`
128/// - `xxx.yyy.zzz`(无前缀)→ `xxx.yyy.zzz`(保持原样)
129fn strip_bearer_prefix(header: &str) -> &str {
130    let trimmed = header.trim();
131    // 大小写不敏感匹配 "bearer"
132    if trimmed.len() >= 6 {
133        let prefix = &trimmed[..6];
134        if prefix.eq_ignore_ascii_case("bearer") {
135            // 跳过 "bearer" 后可能存在的空格
136            return trimmed[6..].trim_start();
137        }
138    }
139    trimmed
140}
141
142/// 使用指定配置验证 JWT token 并提取用户信息
143///
144/// 这是 [`AddonsBaseController::get_token`] 的核心逻辑,抽取为独立函数便于单元测试
145/// 注入不同配置(避免依赖全局 `JWT_CONFIG` 一次性初始化的副作用)。
146///
147/// # 参数
148///
149/// - `authorization`:Authorization header 原始值(可能含 `Bearer ` 前缀)
150/// - `config`:JWT 配置(密钥 + 签发人)
151///
152/// # 返回
153///
154/// - `Ok(Some(UserInfo))`:验证成功
155/// - `Ok(None)`:无 token、密钥未配置、签名/过期/iss 验证失败、缺少 user_id
156fn verify_token_with_config(
157    authorization: Option<&str>,
158    config: &JwtConfig,
159) -> Result<Option<UserInfo>, String> {
160    // 1. 提取 Authorization header 值
161    let header_value = match authorization {
162        Some(v) if !v.is_empty() => v,
163        _ => return Ok(None),
164    };
165
166    // 2. 去除 Bearer 前缀
167    let token = strip_bearer_prefix(header_value).trim();
168    if token.is_empty() {
169        return Ok(None);
170    }
171
172    // 3. 密钥未配置则禁用 JWT 验证
173    if config.secret.is_empty() {
174        return Ok(None);
175    }
176
177    // 4. JwtEncoder 解码并验证签名 + 过期
178    let encoder = sz_orm_auth::jwt::JwtEncoder::new(&config.secret);
179    let claims = match encoder.decode(token) {
180        Ok(c) => c,
181        Err(_) => return Ok(None),
182    };
183
184    // 5. 验证 iss 字段(仅当配置了 issuer 时)
185    if !config.issuer.is_empty() {
186        match &claims.iss {
187            Some(iss) if iss == &config.issuer => { /* 通过 */ }
188            _ => return Ok(None),
189        }
190    }
191
192    // 6. 验证 aud 字段(P1-SEC-10:仅当配置了 audience 时)
193    if !config.audience.is_empty() {
194        match &claims.aud {
195            Some(aud) if aud == &config.audience => { /* 通过 */ }
196            _ => return Ok(None),
197        }
198    }
199
200    // 7. 提取 user_id
201    let user_id = match claims.user_id {
202        Some(id) => id,
203        None => return Ok(None),
204    };
205
206    Ok(Some(UserInfo {
207        user_id,
208        is_login: true,
209    }))
210}
211
212/// 控制器基础 trait(对齐 PHP `app\SzController`)
213///
214/// 业务控制器实现此 trait 即可获得 `render_json` / `render_success` / `render_error`
215/// 与 `post_data` / `get_data` 等方法。
216///
217/// # 用法
218///
219/// ```ignore
220/// use sz_rust_core::controller::SzController;
221/// use sz_rust_core::response::ApiResponse;
222/// use axum::body::Body;
223/// use axum::http::Request;
224/// use axum::response::Response;
225/// use serde_json::json;
226///
227/// struct UserController;
228///
229/// impl SzController for UserController {}
230///
231/// impl UserController {
232///     pub async fn info(&self, req: Request<Body>) -> Response {
233///         let data = self.post_data(req).await.unwrap();
234///         let name = data.get("name").cloned().unwrap_or(json!(null));
235///         self.render_success("success", json!({"name": name}))
236///     }
237/// }
238/// ```
239pub trait SzController: Send + Sync {
240    /// 返回封装后的 API 数据(对应 PHP `renderJson`)
241    ///
242    /// 严格遵循 PHP `compact('code', 'msg', 'data')` 的字段顺序:`code → msg → data`。
243    /// 返回 `Value::Object`(而非 `Response`),便于调用方进一步处理。
244    ///
245    /// # PHP 对齐
246    ///
247    /// ```php
248    /// protected function renderJson($code = 1, $msg = '', $data = []) {
249    ///     return compact('code', 'msg', 'data');
250    /// }
251    /// ```
252    fn render_json(&self, code: i32, msg: impl Into<String>, data: Value) -> Value {
253        let mut map = Map::new();
254        map.insert("code".to_string(), Value::Number(code.into()));
255        map.insert("msg".to_string(), Value::String(msg.into()));
256        map.insert("data".to_string(), data);
257        Value::Object(map)
258    }
259
260    /// 返回操作成功 json(对应 PHP `renderSuccess`)
261    ///
262    /// 参数顺序:`msg, data`(与 PHP 一致,与 `ApiResponse::success(data, msg)` 相反)。
263    /// 返回 `Response`,HTTP 200,Content-Type: application/json; charset=utf-8。
264    ///
265    /// # PHP 对齐
266    ///
267    /// ```php
268    /// protected function renderSuccess($msg = 'success', $data = []) {
269    ///     return json($this->renderJson(1, $msg, $data));
270    /// }
271    /// ```
272    fn render_success(&self, msg: impl Into<String>, data: Value) -> Response {
273        ApiResponse::success(data, msg).into_response()
274    }
275
276    /// 返回操作失败 json(对应 PHP `renderError`)
277    ///
278    /// 参数顺序:`msg, data, code`(与 PHP 一致)。
279    /// 返回 `Response`,HTTP 200(业务失败 HTTP 仍 200,对齐 PHP 行为)。
280    ///
281    /// # PHP 对齐
282    ///
283    /// ```php
284    /// protected function renderError($msg = 'error', $data = [], $code = 0) {
285    ///     return json($this->renderJson($code, $msg, $data));
286    /// }
287    /// ```
288    fn render_error(&self, msg: impl Into<String>, data: Value, code: i32) -> Response {
289        ApiResponse::error_with_code(code, msg, data).into_response()
290    }
291
292    /// 获取合并后的请求参数(对应 PHP `postData()` 无参形式)
293    ///
294    /// 合并 body + query,body 字段优先级高于 query。
295    /// 对齐 PHP `$this->request->param()`(合并 POST + GET + route)。
296    ///
297    /// # PHP 对齐
298    ///
299    /// ```php
300    /// protected function postData($key = null) {
301    ///     return $this->request->param(is_null($key) ? '' : $key . '/a');
302    /// }
303    /// ```
304    fn post_data(&self, req: Request<Body>) -> impl Future<Output = Result<Value, String>> + Send {
305        async move { fetch_post_data(req).await }
306    }
307
308    /// 获取合并参数中指定 key(对应 PHP `postData($key)`)
309    ///
310    /// # 返回
311    ///
312    /// - `Ok(Some(value))`:字段存在
313    /// - `Ok(None)`:字段不存在
314    /// - `Err(String)`:body 读取或 JSON 解析失败
315    fn post_data_by_key(
316        &self,
317        req: Request<Body>,
318        key: &str,
319    ) -> impl Future<Output = Result<Option<Value>, String>> + Send {
320        async move { fetch_post_data_by_key(req, key).await }
321    }
322
323    /// 获取 query 参数(对应 PHP `getData()` 无参形式)
324    ///
325    /// 对齐 PHP `$this->request->get()`。
326    fn get_data(&self, req: &Request<Body>) -> Value {
327        fetch_query_data(req)
328    }
329
330    /// 获取 query 参数中指定 key(对应 PHP `getData($key)`)
331    ///
332    /// # 返回
333    ///
334    /// - `Some(value)`:字段存在
335    /// - `None`:字段不存在
336    fn get_data_by_key(&self, req: &Request<Body>, key: &str) -> Option<Value> {
337        fetch_query_data_by_key(req, key)
338    }
339}
340
341/// 控制器基础 trait(对齐 PHP `app\BaseController`)
342///
343/// PHP 继承链:`BaseController → SzController → AddonsBaseController → 业务控制器`。
344/// Rust 等价:`BaseController: SzController`(trait 继承),业务控制器同时实现
345/// `BaseController` 和 `SzController`(trait 继承自动传播)。
346///
347/// # PHP 对齐
348///
349/// | PHP 属性/方法 | Rust 等价 |
350/// |--------------|-----------|
351/// | `protected $request` | 通过方法参数 `req: Request<Body>` 传入(见 [`SzController`]) |
352/// | `protected $app` | 通过 axum `State<App>` 提取器获取(不在 trait 中) |
353/// | `protected bool $batchValidate = false` | [`BaseController::batch_validate`] |
354/// | `protected array $middleware = []` | [`BaseController::middlewares`] |
355/// | `protected function initialize() {}` | [`BaseController::initialize`] |
356/// | `protected function validate(...)` | [`BaseController::validate`](占位,完整实现) |
357///
358/// # 状态迁移说明
359///
360/// PHP `BaseController` 是 abstract class,包含状态字段($request/$app)。Rust 控制器
361/// 推荐无状态(每个请求通过参数传入 Request),因此本 trait 不持有状态,仅定义行为。
362pub trait BaseController: SzController {
363    /// 是否批量验证(对齐 PHP `protected bool $batchValidate = false`)
364    ///
365    /// 返回 `true` 时,`validate` 方法会收集所有错误后一次性返回;
366    /// 返回 `false` 时,遇到第一个错误即返回。
367    fn batch_validate(&self) -> bool {
368        false
369    }
370
371    /// 控制器中间件列表(对齐 PHP `protected array $middleware = []`)
372    ///
373    /// 返回中间件标识符列表(如 `["auth", "cors"]`)。
374    /// 实际中间件注册在中间件系统中处理。
375    fn middlewares(&self) -> Vec<String> {
376        Vec::new()
377    }
378
379    /// 初始化钩子(对齐 PHP `protected function initialize() {}`)
380    ///
381    /// 在控制器实例化后、方法调用前执行。
382    /// 子类可重写以执行初始化逻辑(如设置默认值、加载配置)。
383    ///
384    /// # PHP 对齐
385    ///
386    /// ```php
387    /// protected function initialize() {
388    ///     // 子类重写
389    /// }
390    /// ```
391    fn initialize(&self) {}
392
393    /// 数据验证(对齐 PHP `protected function validate(...)`)
394    ///
395    /// # PHP 对齐
396    ///
397    /// ```php
398    /// protected function validate(array $data, $validate, array $message = [], bool $batch = false) {
399    ///     // ... 完整验证逻辑
400    /// }
401    /// ```
402    ///
403    /// # 实现说明
404    ///
405    /// 将 `rules` 与 `messages` 转换为内部 [`Validate`] 构建器调用,
406    /// 批量模式由 [`BaseController::batch_validate`] 决定(对齐 PHP `$batch`
407    /// 参数默认值 `false`,但 Rust 通过 trait 方法覆盖以适配无状态控制器)。
408    ///
409    /// # 参数
410    ///
411    /// - `data`:待验证的数据
412    /// - `rules`:验证规则列表,元组格式 `(字段名, 规则字符串)`
413    ///   - 例:`[("name", "require"), ("age", "require|integer|gt:0")]`
414    /// - `messages`:错误消息列表,元组格式 `(规则键, 消息)`
415    ///   - 例:`[("name.require", "名称必填"), ("age.integer", "年龄须为整数")]`
416    ///
417    /// # 返回
418    ///
419    /// - `Ok(())`:验证通过
420    /// - `Err(String)`:验证失败,包含错误消息(批量模式下以 `; ` 分隔多条错误)
421    fn validate(
422        &self,
423        data: &Value,
424        rules: &[(&str, &str)],
425        messages: &[(&str, &str)],
426    ) -> Result<(), String> {
427        // 构建 Validate 实例,将 (字段名, 规则) 元组列表逐条注册
428        let mut validator = Validate::new();
429        for (name, rule) in rules {
430            validator = validator.rule(name, rule);
431        }
432
433        // 将 (规则键, 消息) 元组列表合并到 IndexMap 后注入
434        let mut msg_map = IndexMap::new();
435        for (key, msg) in messages {
436            msg_map.insert(key.to_string(), msg.to_string());
437        }
438        validator = validator.message(msg_map);
439
440        // 批量模式由控制器配置决定(对齐 PHP $batch 参数)
441        if self.batch_validate() {
442            validator = validator.batch(true);
443        }
444
445        // 执行验证并转换错误类型(Display 实现已处理 Single/Batch 两种格式)
446        match validator.check(data) {
447            Ok(()) => Ok(()),
448            Err(err) => Err(err.to_string()),
449        }
450    }
451}
452
453/// 路由信息(对齐 PHP `addons\BaseController` 的 $controller/$action/$routeUri/$group)
454///
455/// 由 [`AddonsBaseController::parse_route_info`] 从 URI 解析得到。
456#[derive(Debug, Clone, PartialEq, Eq)]
457pub struct RouteInfo {
458    /// 当前控制器名称(对齐 PHP `$this->controller`)
459    pub controller: String,
460    /// 当前方法名称(对齐 PHP `$this->action`)
461    pub action: String,
462    /// 当前路由 URI(对齐 PHP `$this->routeUri`,格式 `/controller/action`)
463    pub route_uri: String,
464    /// 控制器分组(对齐 PHP `$this->group`,controller 的第一段)
465    pub group: String,
466}
467
468/// 用户信息(对齐 PHP `$this->user` 数组中的关键字段)
469///
470/// JWT 验证成功后返回,包含 user_id 与 is_login 状态。
471#[derive(Debug, Clone, PartialEq, Eq)]
472pub struct UserInfo {
473    /// 用户 ID
474    pub user_id: i64,
475    /// 是否已登录(对齐 PHP `$this->user['is_login'] == 1`)
476    pub is_login: bool,
477}
478
479/// addons 控制器基础 trait(对齐 PHP `addons\BaseController`)
480///
481/// PHP 继承链:`BaseController → SzController → AddonsBaseController → 业务控制器`。
482/// Rust 等价:`AddonsBaseController: BaseController: SzController`(trait 继承链)。
483///
484/// # PHP 对齐
485///
486/// | PHP 属性/方法 | Rust 等价 |
487/// |--------------|-----------|
488/// | `protected $user` | handler 中通过 [`AddonsBaseController::get_token`] 获取 [`UserInfo`] |
489/// | `protected string $controller` | [`RouteInfo::controller`](由 [`AddonsBaseController::parse_route_info`] 解析) |
490/// | `protected string $action` | [`RouteInfo::action`] |
491/// | `protected string $routeUri` | [`RouteInfo::route_uri`] |
492/// | `protected string $group` | [`RouteInfo::group`] |
493/// | `protected array $allowAllAction` | [`AddonsBaseController::allow_all_action`] |
494/// | `public function initialize()` | 由 handler 显式调用 `parse_route_info` + `get_token` + `check_login` |
495/// | `public function getToken()` | [`AddonsBaseController::get_token`](占位,JWT 完整实现) |
496/// | `protected function getRouteinfo()` | [`AddonsBaseController::parse_route_info`] |
497/// | `private function checkLogin()` | [`AddonsBaseController::check_login`] |
498///
499/// # 状态迁移说明
500///
501/// PHP `AddonsBaseController::initialize()` 在构造函数中自动调用,执行
502/// `getRouteinfo() + getToken() + checkLogin()`。Rust 控制器无状态,
503/// handler 需显式调用这三个方法(顺序:parse_route_info → get_token → check_login)。
504pub trait AddonsBaseController: BaseController {
505    /// 登录验证白名单(对齐 PHP `protected array $allowAllAction`)
506    ///
507    /// 默认包含 `/passport/login` 和 `/task/task/userClerk`。
508    /// 子类可覆盖以添加更多白名单路径。
509    fn allow_all_action(&self) -> Vec<&'static str> {
510        vec!["/passport/login", "/task/task/userClerk"]
511    }
512
513    /// 解析路由信息(对齐 PHP `protected function getRouteinfo()`)
514    ///
515    /// 从 URI 路径解析 controller/action/group/route_uri。
516    ///
517    /// # 解析规则
518    ///
519    /// - URI 形如 `/controller/action`:controller=controller, action=action, group=controller
520    /// - URI 形如 `/group/controller/action`:controller=group/controller, action=action, group=group/controller
521    /// - URI 形如 `/controller`:controller=controller, action="", group=controller
522    /// - URI 形如 `/`:所有字段为空字符串
523    ///
524    /// # PHP 对齐(含 bug 复刻)
525    ///
526    /// ```php
527    /// protected function getRouteinfo(): void {
528    ///     $this->controller = toUnderScore(Request()->controller());
529    ///     $this->controller = str_replace(".", "/", $this->controller);
530    ///     $this->controller = str_replace("_", "", $this->controller);
531    ///     $this->action = Request()->action();
532    ///     // ⚠️ PHP bug:str_replace 已将 "." 替换为 "/",但 strstr 仍以 "." 为分隔符,
533    ///     // 永远返回 false,因此 $this->group === $this->controller(始终相等)。
534    ///     $groupstr = strstr($this->controller, '.', true);
535    ///     $this->group = $groupstr !== false ? $groupstr : $this->controller;
536    ///     $this->routeUri = '/' . $this->controller . '/' . $this->action;
537    /// }
538    /// ```
539    ///
540    /// **PHP bug 复刻说明**:Rust 严格对齐 PHP 行为,`group` 字段始终等于 `controller`。
541    /// 经核查 PHP 后端全部源码,`$this->group` 为只写不读的死字段,bug 不暴露,
542    /// 但为了 R5(PHP/Rust 行为对比)严格一致性,仍复刻此行为。
543    ///
544    /// 注意:PHP 使用 ThinkPHP 的 `Request()->controller()` 获取控制器名,
545    /// Rust 直接从 URI 路径解析(已通过路由匹配)。
546    fn parse_route_info(&self, uri: &str) -> RouteInfo {
547        let path = uri.split('?').next().unwrap_or("");
548        let path = path.trim_start_matches('/');
549        let segments: Vec<&str> = if path.is_empty() {
550            Vec::new()
551        } else {
552            path.split('/').collect()
553        };
554
555        let (controller, action) = match segments.len() {
556            0 => (String::new(), String::new()),
557            1 => (segments[0].to_string(), String::new()),
558            _ => (
559                segments[..segments.len() - 1].join("/"),
560                segments[segments.len() - 1].to_string(),
561            ),
562        };
563
564        // PHP bug 复刻:group === controller(详见方法文档注释)
565        let group = controller.clone();
566
567        let route_uri = if controller.is_empty() && action.is_empty() {
568            "/".to_string()
569        } else {
570            format!("/{controller}/{action}")
571        };
572
573        RouteInfo {
574            controller,
575            action,
576            route_uri,
577            group,
578        }
579    }
580
581    /// 检查登录状态(对齐 PHP `private function checkLogin()`)
582    ///
583    /// # 行为
584    ///
585    /// 1. 若 `route_uri` 在白名单中,返回 `Ok(())`
586    /// 2. 若 `user_is_login == true`,返回 `Ok(())`
587    /// 3. 否则返回 `Err("not_login")`
588    ///
589    /// # PHP 对齐
590    ///
591    /// ```php
592    /// private function checkLogin(): void {
593    ///     if (in_array($this->routeUri, $this->allowAllAction)) {
594    ///         return;
595    ///     }
596    ///     if(!empty($this->user)){
597    ///         if($this->user['is_login'] == 1){
598    ///             return;
599    ///         }
600    ///     }
601    ///     throw new BaseException(['code' => -1, 'msg' => 'not_login']);
602    /// }
603    /// ```
604    fn check_login(&self, route_uri: &str, user_is_login: bool) -> Result<(), String> {
605        if self.allow_all_action().contains(&route_uri) {
606            return Ok(());
607        }
608        if user_is_login {
609            return Ok(());
610        }
611        Err("not_login".to_string())
612    }
613
614    /// 获取 token 用户信息(对齐 PHP `public function getToken()`)
615    ///
616    /// # 实现说明
617    ///
618    /// 使用 sz-orm-auth 的 `JwtEncoder` 进行 HS256 签名验证与过期检查,
619    /// 配置项(`SZ_JWT_SECRET` / `SZ_JWT_ISSUER`)在启动时从环境变量读取。
620    /// 核心验证逻辑见 `verify_token_with_config`,便于单元测试注入配置。
621    ///
622    /// 验证流程(对齐 PHP `Token::getUserId`):
623    /// 1. 提取 Authorization header 中的 Bearer token
624    /// 2. 通过 `JwtEncoder::decode` 验证签名 + 过期时间
625    /// 3. 验证 `iss` 字段匹配配置的签发人
626    /// 4. 提取 `user_id` claim 返回 [`UserInfo`]
627    ///
628    /// # PHP 对齐
629    ///
630    /// ```php
631    /// public function getToken(){
632    ///     if (!$token = Token::getUserId(request()->header('Authorization'))) {
633    ///         if(in_array($this->routeUri, $this->allowAllAction)) {
634    ///             return true;
635    ///         } else {
636    ///             throw new BaseException(['msg' => '缺少必要的参数,请重新登陆!']);
637    ///         }
638    ///     }
639    ///     return $token;
640    /// }
641    /// ```
642    ///
643    /// # 参数
644    ///
645    /// - `authorization`:`Authorization` 请求头的值(如 `"Bearer xxx.yyy.zzz"`)
646    ///
647    /// # 返回
648    ///
649    /// - `Ok(Some(UserInfo))`:JWT 验证成功,返回用户信息
650    /// - `Ok(None)`:无 token、token 为空、签名密钥未配置或验证失败
651    ///   (调用方根据 route_uri 决定是否抛错,对齐 PHP `if (!$token)` 分支)
652    /// - `Err(String)`:JWT 解析过程中出现异常(如格式错误)
653    fn get_token(&self, authorization: Option<&str>) -> Result<Option<UserInfo>, String> {
654        // 委托给 verify_token_with_config,使用全局 JWT_CONFIG
655        // 抽取独立函数便于单元测试注入不同配置(避免 once_cell 一次性初始化限制)
656        // JWT_CONFIG 为 None 时(未配置 SZ_JWT_SECRET)跳过验证,返回 Ok(None)
657        match JWT_CONFIG.as_ref() {
658            Some(config) => verify_token_with_config(authorization, config),
659            None => Ok(None),
660        }
661    }
662}
663
664#[cfg(test)]
665mod tests {
666    use super::*;
667    use axum::body::Body;
668    use axum::http::{Method, Request, StatusCode};
669    use http_body_util::BodyExt;
670    use serde_json::json;
671    use tower::ServiceExt;
672
673    /// 测试用 Mock 控制器(仅使用默认实现)
674    struct MockController;
675
676    impl SzController for MockController {}
677
678    fn make_json_request(body: &str, query: Option<&str>) -> Request<Body> {
679        let uri = match query {
680            Some(q) => format!("/?{q}"),
681            None => "/".to_string(),
682        };
683        Request::builder()
684            .method(Method::POST)
685            .uri(&uri)
686            .header("content-type", "application/json")
687            .body(Body::from(body.to_string()))
688            .unwrap()
689    }
690
691    fn make_get_request(query: &str) -> Request<Body> {
692        Request::builder()
693            .method(Method::GET)
694            .uri(format!("/?{query}"))
695            .body(Body::empty())
696            .unwrap()
697    }
698
699    async fn collect_body(resp: Response) -> String {
700        let bytes = resp.into_body().collect().await.unwrap().to_bytes();
701        String::from_utf8(bytes.to_vec()).unwrap()
702    }
703
704    // ====================================================================
705    // render_json 测试
706    // ====================================================================
707
708    #[test]
709    fn test_render_json_field_order() {
710        // 严格验证字段顺序:code → msg → data(对齐 PHP compact())
711        let ctrl = MockController;
712        let value = ctrl.render_json(1, "ok", json!({"id": 1}));
713        let obj = value.as_object().unwrap();
714        let keys: Vec<&String> = obj.keys().collect();
715        assert_eq!(keys, vec!["code", "msg", "data"]);
716    }
717
718    #[test]
719    fn test_render_json_default_values() {
720        // PHP 默认值:$code=1, $msg='', $data=[]
721        let ctrl = MockController;
722        let value = ctrl.render_json(1, "", Value::Object(Map::new()));
723        assert_eq!(value["code"], 1);
724        assert_eq!(value["msg"], "");
725        assert!(value["data"].is_object());
726        assert!(value["data"].as_object().unwrap().is_empty());
727    }
728
729    #[test]
730    fn test_render_json_custom_code() {
731        let ctrl = MockController;
732        let value = ctrl.render_json(-1, "未登录", json!({}));
733        assert_eq!(value["code"], -1);
734        assert_eq!(value["msg"], "未登录");
735    }
736
737    #[test]
738    fn test_render_json_compact_semantics() {
739        // 验证 PHP compact('code','msg','data') 的等价语义:
740        // 变量名 → 值的映射,保序序列化
741        let ctrl = MockController;
742        let value = ctrl.render_json(0, "失败", json!({"field": "name"}));
743        let json_str = value.to_string();
744        // 字段顺序必须是 code → msg → data
745        assert_eq!(
746            json_str,
747            r#"{"code":0,"msg":"失败","data":{"field":"name"}}"#
748        );
749    }
750
751    #[test]
752    fn test_render_json_returns_value_not_response() {
753        // PHP renderJson 返回数组(非 Response),renderSuccess/renderError 才返回 json()
754        let ctrl = MockController;
755        let value = ctrl.render_json(1, "ok", json!({}));
756        // 应该是 Value,不是 Response
757        assert!(value.is_object());
758    }
759
760    // ====================================================================
761    // render_success 测试
762    // ====================================================================
763
764    #[test]
765    fn test_render_success_returns_response() {
766        let ctrl = MockController;
767        let resp = ctrl.render_success("success", json!({"id": 1}));
768        assert_eq!(resp.status(), StatusCode::OK);
769        assert_eq!(
770            resp.headers().get("content-type").unwrap(),
771            "application/json; charset=utf-8"
772        );
773    }
774
775    #[tokio::test]
776    async fn test_render_success_body_format() {
777        // 对齐 PHP renderSuccess('success', $data)
778        let ctrl = MockController;
779        let resp = ctrl.render_success("success", json!({"id": 1, "name": "alice"}));
780        let body = collect_body(resp).await;
781        assert_eq!(
782            body,
783            r#"{"code":1,"msg":"success","data":{"id":1,"name":"alice"}}"#
784        );
785    }
786
787    #[tokio::test]
788    async fn test_render_success_default_msg() {
789        // PHP 默认 msg='success'
790        let ctrl = MockController;
791        let resp = ctrl.render_success("success", json!({}));
792        let body = collect_body(resp).await;
793        assert_eq!(body, r#"{"code":1,"msg":"success","data":{}}"#);
794    }
795
796    #[tokio::test]
797    async fn test_render_success_via_axum_router() {
798        // 端到端验证:通过 axum Router 调用 render_success
799        struct UserController;
800        impl SzController for UserController {}
801
802        async fn handler() -> Response {
803            let ctrl = UserController;
804            ctrl.render_success("ok", json!({"id": 1}))
805        }
806
807        let router = axum::Router::new().route("/", axum::routing::get(handler));
808        let req = Request::builder()
809            .method(Method::GET)
810            .uri("/")
811            .body(Body::empty())
812            .unwrap();
813        let resp = router.oneshot(req).await.unwrap();
814
815        assert_eq!(resp.status(), StatusCode::OK);
816        let body = collect_body(resp).await;
817        assert_eq!(body, r#"{"code":1,"msg":"ok","data":{"id":1}}"#);
818    }
819
820    // ====================================================================
821    // render_error 测试
822    // ====================================================================
823
824    #[test]
825    fn test_render_error_returns_response() {
826        let ctrl = MockController;
827        let resp = ctrl.render_error("error", json!({}), 0);
828        assert_eq!(resp.status(), StatusCode::OK); // 业务错误 HTTP 仍 200
829    }
830
831    #[tokio::test]
832    async fn test_render_error_default_code() {
833        // PHP 默认 $code=0
834        let ctrl = MockController;
835        let resp = ctrl.render_error("参数错误", json!({}), 0);
836        let body = collect_body(resp).await;
837        assert_eq!(body, r#"{"code":0,"msg":"参数错误","data":{}}"#);
838    }
839
840    #[tokio::test]
841    async fn test_render_error_custom_code() {
842        // PHP renderError($msg, $data, $code=-1) 用于未登录场景
843        let ctrl = MockController;
844        let resp = ctrl.render_error("not_login", json!({}), -1);
845        let body = collect_body(resp).await;
846        assert_eq!(body, r#"{"code":-1,"msg":"not_login","data":{}}"#);
847    }
848
849    #[tokio::test]
850    async fn test_render_error_with_data() {
851        let ctrl = MockController;
852        let resp = ctrl.render_error("失败", json!({"field": "name"}), 0);
853        let body = collect_body(resp).await;
854        assert_eq!(body, r#"{"code":0,"msg":"失败","data":{"field":"name"}}"#);
855    }
856
857    #[tokio::test]
858    async fn test_render_error_via_axum_router() {
859        struct UserController;
860        impl SzController for UserController {}
861
862        async fn handler() -> Response {
863            let ctrl = UserController;
864            ctrl.render_error("参数错误", json!({}), 0)
865        }
866
867        let router = axum::Router::new().route("/", axum::routing::post(handler));
868        let req = Request::builder()
869            .method(Method::POST)
870            .uri("/")
871            .body(Body::empty())
872            .unwrap();
873        let resp = router.oneshot(req).await.unwrap();
874
875        assert_eq!(resp.status(), StatusCode::OK);
876        let body = collect_body(resp).await;
877        assert_eq!(body, r#"{"code":0,"msg":"参数错误","data":{}}"#);
878    }
879
880    // ====================================================================
881    // post_data 测试(对齐 PHP postData())
882    // ====================================================================
883
884    #[tokio::test]
885    async fn test_post_data_json_body() {
886        let ctrl = MockController;
887        let req = make_json_request(r#"{"name":"alice","age":30}"#, None);
888        let data = ctrl.post_data(req).await.unwrap();
889        assert_eq!(data["name"], "alice");
890        assert_eq!(data["age"], 30);
891    }
892
893    #[tokio::test]
894    async fn test_post_data_query_only() {
895        let ctrl = MockController;
896        let req = make_json_request("", Some("page=1&size=10"));
897        let data = ctrl.post_data(req).await.unwrap();
898        assert_eq!(data["page"], "1");
899        assert_eq!(data["size"], "10");
900    }
901
902    #[tokio::test]
903    async fn test_post_data_body_overrides_query() {
904        // body 优先级高于 query(对齐 PHP param() 行为)
905        let ctrl = MockController;
906        let req = make_json_request(r#"{"page":99}"#, Some("page=1&size=10"));
907        let data = ctrl.post_data(req).await.unwrap();
908        assert_eq!(data["page"], 99);
909        assert_eq!(data["size"], "10");
910    }
911
912    #[tokio::test]
913    async fn test_post_data_by_key_exists() {
914        let ctrl = MockController;
915        let req = make_json_request(r#"{"name":"alice","age":30}"#, None);
916        let name = ctrl.post_data_by_key(req, "name").await.unwrap();
917        assert_eq!(name, Some(json!("alice")));
918    }
919
920    #[tokio::test]
921    async fn test_post_data_by_key_missing() {
922        let ctrl = MockController;
923        let req = make_json_request(r#"{"name":"alice"}"#, None);
924        let age = ctrl.post_data_by_key(req, "age").await.unwrap();
925        assert_eq!(age, None);
926    }
927
928    // ====================================================================
929    // get_data 测试(对齐 PHP getData())
930    // ====================================================================
931
932    #[test]
933    fn test_get_data_query() {
934        let ctrl = MockController;
935        let req = make_get_request("page=1&size=10");
936        let data = ctrl.get_data(&req);
937        assert_eq!(data["page"], "1");
938        assert_eq!(data["size"], "10");
939    }
940
941    #[test]
942    fn test_get_data_empty_query() {
943        let ctrl = MockController;
944        let req = make_get_request("");
945        let data = ctrl.get_data(&req);
946        assert!(data.as_object().unwrap().is_empty());
947    }
948
949    #[test]
950    fn test_get_data_by_key_exists() {
951        let ctrl = MockController;
952        let req = make_get_request("page=1&size=10");
953        let page = ctrl.get_data_by_key(&req, "page");
954        assert_eq!(page, Some(json!("1")));
955    }
956
957    #[test]
958    fn test_get_data_by_key_missing() {
959        let ctrl = MockController;
960        let req = make_get_request("page=1");
961        let size = ctrl.get_data_by_key(&req, "size");
962        assert_eq!(size, None);
963    }
964
965    // ====================================================================
966    // 控制器多实例隔离测试
967    // ====================================================================
968
969    #[tokio::test]
970    async fn test_multiple_controllers_independent() {
971        // 验证多个控制器实例互不干扰(无状态 trait)
972        struct CtrlA;
973        struct CtrlB;
974        impl SzController for CtrlA {}
975        impl SzController for CtrlB {}
976
977        let a = CtrlA;
978        let b = CtrlB;
979
980        let req_a = make_json_request(r#"{"k":"a"}"#, None);
981        let req_b = make_json_request(r#"{"k":"b"}"#, None);
982
983        let data_a = a.post_data(req_a).await.unwrap();
984        let data_b = b.post_data(req_b).await.unwrap();
985
986        assert_eq!(data_a["k"], "a");
987        assert_eq!(data_b["k"], "b");
988
989        // 验证 render_json 互不影响
990        let va = a.render_json(1, "a", json!({}));
991        let vb = b.render_json(0, "b", json!({}));
992        assert_eq!(va["code"], 1);
993        assert_eq!(va["msg"], "a");
994        assert_eq!(vb["code"], 0);
995        assert_eq!(vb["msg"], "b");
996    }
997
998    // ====================================================================
999    // PHP 一致性综合测试
1000    // ====================================================================
1001
1002    #[tokio::test]
1003    async fn test_php_consistency_full_flow() {
1004        // 模拟 PHP 控制器典型流程:读取 postData → 业务处理 → renderSuccess
1005        struct OrderController;
1006        impl SzController for OrderController {}
1007
1008        let ctrl = OrderController;
1009
1010        // 1. 读取 POST 数据
1011        let req = make_json_request(r#"{"order_id":12345,"amount":99.5}"#, None);
1012        let data = ctrl.post_data(req).await.unwrap();
1013        let order_id = data["order_id"].as_i64().unwrap();
1014        let amount = data["amount"].as_f64().unwrap();
1015
1016        // 2. 业务处理(模拟)
1017        let result = json!({
1018            "order_id": order_id,
1019            "amount": amount,
1020            "status": "paid"
1021        });
1022
1023        // 3. 返回成功响应
1024        let resp = ctrl.render_success("支付成功", result);
1025        let body = collect_body(resp).await;
1026
1027        // 验证字段顺序与 PHP compact() 一致
1028        assert_eq!(
1029            body,
1030            r#"{"code":1,"msg":"支付成功","data":{"order_id":12345,"amount":99.5,"status":"paid"}}"#
1031        );
1032    }
1033
1034    #[tokio::test]
1035    async fn test_php_consistency_error_flow() {
1036        // 模拟 PHP 控制器错误流程:校验失败 → renderError
1037        struct UserController;
1038        impl SzController for UserController {}
1039
1040        let ctrl = UserController;
1041
1042        let req = make_json_request(r#"{"name":""}"#, None);
1043        let data = ctrl.post_data(req).await.unwrap();
1044        let name = data["name"].as_str().unwrap();
1045
1046        if name.is_empty() {
1047            let resp = ctrl.render_error("用户名不能为空", json!({"field": "name"}), 0);
1048            let body = collect_body(resp).await;
1049            assert_eq!(
1050                body,
1051                r#"{"code":0,"msg":"用户名不能为空","data":{"field":"name"}}"#
1052            );
1053        } else {
1054            panic!("should be empty");
1055        }
1056    }
1057
1058    #[tokio::test]
1059    async fn test_php_consistency_not_login_flow() {
1060        // 模拟 PHP 控制器未登录流程:renderError($msg, $data, $code=-1)
1061        struct PassportController;
1062        impl SzController for PassportController {}
1063
1064        let ctrl = PassportController;
1065        let resp = ctrl.render_error("not_login", json!({}), -1);
1066        let body = collect_body(resp).await;
1067        assert_eq!(body, r#"{"code":-1,"msg":"not_login","data":{}}"#);
1068    }
1069
1070    // ====================================================================
1071    // BaseController trait 测试
1072    // ====================================================================
1073
1074    /// 实现 BaseController 的 Mock 控制器(同时自动实现 SzController)
1075    struct MockBaseController;
1076
1077    impl SzController for MockBaseController {}
1078    impl BaseController for MockBaseController {}
1079
1080    #[test]
1081    fn test_base_controller_default_batch_validate() {
1082        // PHP 默认 $batchValidate = false
1083        let ctrl = MockBaseController;
1084        assert!(!ctrl.batch_validate());
1085    }
1086
1087    #[test]
1088    fn test_base_controller_default_middlewares_empty() {
1089        // PHP 默认 $middleware = []
1090        let ctrl = MockBaseController;
1091        assert!(ctrl.middlewares().is_empty());
1092    }
1093
1094    #[test]
1095    fn test_base_controller_default_initialize_no_panic() {
1096        // PHP 默认 initialize() 为空,调用应无副作用
1097        let ctrl = MockBaseController;
1098        ctrl.initialize(); // 不应 panic
1099    }
1100
1101    #[test]
1102    fn test_base_controller_default_validate_returns_ok() {
1103        // require 规则 + 字段存在 → 应通过
1104        let ctrl = MockBaseController;
1105        let data = json!({"name": "alice"});
1106        let rules = [("name", "require")];
1107        let messages: [(&str, &str); 0] = [];
1108        let result = ctrl.validate(&data, &rules, &messages);
1109        assert!(result.is_ok());
1110    }
1111
1112    #[test]
1113    fn test_validate_require_pass_with_value() {
1114        // require 规则:字段存在且非空 → 通过
1115        let ctrl = MockBaseController;
1116        let data = json!({"name": "alice", "age": 30});
1117        let rules = [("name", "require"), ("age", "require|integer")];
1118        let messages: [(&str, &str); 0] = [];
1119        assert!(ctrl.validate(&data, &rules, &messages).is_ok());
1120    }
1121
1122    #[test]
1123    fn test_validate_require_fail_when_missing() {
1124        // require 规则:字段缺失 → 失败(Single 模式,返回第一条错误)
1125        let ctrl = MockBaseController;
1126        let data = json!({"name": "alice"});
1127        let rules = [("name", "require"), ("age", "require|integer")];
1128        let messages: [(&str, &str); 0] = [];
1129        let result = ctrl.validate(&data, &rules, &messages);
1130        assert!(result.is_err());
1131        let err = result.unwrap_err();
1132        // 错误信息应包含 age 字段(require 验证失败)
1133        assert!(err.contains("age"), "error: {err}");
1134    }
1135
1136    #[test]
1137    fn test_validate_integer_fail_on_string() {
1138        // integer 规则:字段非整数 → 失败
1139        let ctrl = MockBaseController;
1140        let data = json!({"age": "not-a-number"});
1141        let rules = [("age", "require|integer")];
1142        let messages: [(&str, &str); 0] = [];
1143        let result = ctrl.validate(&data, &rules, &messages);
1144        assert!(result.is_err());
1145    }
1146
1147    #[test]
1148    fn test_validate_custom_message_applied() {
1149        // 自定义消息应被使用(对齐 PHP message[field.type])
1150        let ctrl = MockBaseController;
1151        let data = json!({}); // name 缺失
1152        let rules = [("name", "require")];
1153        let messages = [("name.require", "名称必填")];
1154        let result = ctrl.validate(&data, &rules, &messages);
1155        assert!(result.is_err());
1156        assert_eq!(result.unwrap_err(), "名称必填");
1157    }
1158
1159    #[test]
1160    fn test_validate_batch_mode_returns_multiple_errors() {
1161        // 批量模式:收集所有错误,以 "; " 分隔
1162        struct BatchController;
1163        impl SzController for BatchController {}
1164        impl BaseController for BatchController {
1165            fn batch_validate(&self) -> bool {
1166                true
1167            }
1168        }
1169
1170        let ctrl = BatchController;
1171        let data = json!({}); // name 和 age 都缺失
1172        let rules = [("name", "require"), ("age", "require")];
1173        let messages = [("name.require", "名称必填"), ("age.require", "年龄必填")];
1174        let result = ctrl.validate(&data, &rules, &messages);
1175        assert!(result.is_err());
1176        let err = result.unwrap_err();
1177        // 两条错误都应出现(顺序按字段注册顺序)
1178        assert!(err.contains("名称必填"), "err: {err}");
1179        assert!(err.contains("年龄必填"), "err: {err}");
1180        // 应以 "; " 分隔(ValidateError::Batch 的 Display 格式)
1181        assert!(err.contains("; "), "err: {err}");
1182    }
1183
1184    #[test]
1185    fn test_validate_single_mode_returns_first_error_only() {
1186        // 非批量模式:仅返回第一条错误
1187        let ctrl = MockBaseController;
1188        let data = json!({}); // name 和 age 都缺失
1189        let rules = [("name", "require"), ("age", "require")];
1190        let messages = [("name.require", "名称必填"), ("age.require", "年龄必填")];
1191        let result = ctrl.validate(&data, &rules, &messages);
1192        assert!(result.is_err());
1193        let err = result.unwrap_err();
1194        // 仅包含第一条错误,不包含第二条
1195        assert!(err.contains("名称必填"), "err: {err}");
1196        assert!(!err.contains("年龄必填"), "err: {err}");
1197    }
1198
1199    #[test]
1200    fn test_validate_in_rule_pass() {
1201        // in 规则:值在列表中 → 通过
1202        let ctrl = MockBaseController;
1203        let data = json!({"status": "active"});
1204        let rules = [("status", "require|in:active,inactive")];
1205        let messages: [(&str, &str); 0] = [];
1206        assert!(ctrl.validate(&data, &rules, &messages).is_ok());
1207    }
1208
1209    #[test]
1210    fn test_validate_in_rule_fail() {
1211        // in 规则:值不在列表中 → 失败
1212        let ctrl = MockBaseController;
1213        let data = json!({"status": "deleted"});
1214        let rules = [("status", "require|in:active,inactive")];
1215        let messages: [(&str, &str); 0] = [];
1216        assert!(ctrl.validate(&data, &rules, &messages).is_err());
1217    }
1218
1219    #[test]
1220    fn test_validate_empty_rules_always_pass() {
1221        // 空规则列表:无验证规则,总应通过
1222        let ctrl = MockBaseController;
1223        let data = json!({"anything": "value"});
1224        let rules: [(&str, &str); 0] = [];
1225        let messages: [(&str, &str); 0] = [];
1226        assert!(ctrl.validate(&data, &rules, &messages).is_ok());
1227    }
1228
1229    #[test]
1230    fn test_base_controller_inherits_sz_controller_methods() {
1231        // BaseController: SzController,自动获得 render_json 等方法
1232        let ctrl = MockBaseController;
1233        let value = ctrl.render_json(1, "ok", json!({}));
1234        assert_eq!(value["code"], 1);
1235        assert_eq!(value["msg"], "ok");
1236
1237        let resp = ctrl.render_success("ok", json!({"id": 1}));
1238        assert_eq!(resp.status(), StatusCode::OK);
1239    }
1240
1241    /// 覆盖默认值的子类控制器
1242    struct CustomController;
1243
1244    impl SzController for CustomController {}
1245    impl BaseController for CustomController {
1246        fn batch_validate(&self) -> bool {
1247            true
1248        }
1249
1250        fn middlewares(&self) -> Vec<String> {
1251            vec!["auth".to_string(), "cors".to_string()]
1252        }
1253
1254        fn initialize(&self) {
1255            // 子类自定义初始化(无状态,仅作演示)
1256        }
1257    }
1258
1259    #[test]
1260    fn test_base_controller_override_batch_validate() {
1261        let ctrl = CustomController;
1262        assert!(ctrl.batch_validate());
1263    }
1264
1265    #[test]
1266    fn test_base_controller_override_middlewares() {
1267        let ctrl = CustomController;
1268        let mws = ctrl.middlewares();
1269        assert_eq!(mws, vec!["auth", "cors"]);
1270    }
1271
1272    #[test]
1273    fn test_base_controller_override_initialize() {
1274        let ctrl = CustomController;
1275        ctrl.initialize(); // 不应 panic
1276    }
1277
1278    /// 模拟 PHP 业务控制器:带状态的 initialize 钩子
1279    /// PHP 中 $this->batchValidate = true; 在 initialize() 中设置
1280    struct StatefulController {
1281        initialized: parking_lot::Mutex<bool>,
1282        custom_batch: bool,
1283    }
1284
1285    impl StatefulController {
1286        fn new() -> Self {
1287            Self {
1288                initialized: parking_lot::Mutex::new(false),
1289                custom_batch: false,
1290            }
1291        }
1292    }
1293
1294    impl SzController for StatefulController {}
1295    impl BaseController for StatefulController {
1296        fn batch_validate(&self) -> bool {
1297            self.custom_batch
1298        }
1299
1300        fn initialize(&self) {
1301            *self.initialized.lock() = true;
1302            // 模拟 PHP: $this->batchValidate = true;
1303            // Rust 中由于 trait 方法不能修改 self,需要在调用方处理
1304        }
1305    }
1306
1307    #[test]
1308    fn test_base_controller_stateful_initialize() {
1309        let ctrl = StatefulController::new();
1310        assert!(!*ctrl.initialized.lock()); // 初始未初始化
1311        ctrl.initialize(); // 调用初始化钩子
1312        assert!(*ctrl.initialized.lock()); // 已初始化
1313    }
1314
1315    /// 模拟 PHP 控制器典型流程:initialize → validate → renderSuccess
1316    #[tokio::test]
1317    async fn test_base_controller_php_full_flow() {
1318        struct UserController;
1319        impl SzController for UserController {}
1320        impl BaseController for UserController {}
1321
1322        let ctrl = UserController;
1323
1324        // 1. 初始化钩子
1325        ctrl.initialize();
1326
1327        // 2. 读取 POST 数据
1328        let req = make_json_request(r#"{"name":"alice","age":30}"#, None);
1329        let data = ctrl.post_data(req).await.unwrap();
1330
1331        // 3. 验证(validate() 已实现 require/integer/gt 等规则)
1332        let rules = [("name", "require"), ("age", "require|integer|gt:0")];
1333        let messages: [(&str, &str); 0] = [];
1334        ctrl.validate(&data, &rules, &messages).unwrap();
1335
1336        // 4. 返回成功响应
1337        let resp =
1338            ctrl.render_success("success", json!({"name": data["name"], "age": data["age"]}));
1339        let body = collect_body(resp).await;
1340        assert_eq!(
1341            body,
1342            r#"{"code":1,"msg":"success","data":{"name":"alice","age":30}}"#
1343        );
1344    }
1345
1346    /// 测试多控制器继承链独立性
1347    #[test]
1348    fn test_base_controller_multiple_instances_independent() {
1349        struct ControllerA;
1350        struct ControllerB;
1351
1352        impl SzController for ControllerA {}
1353        impl BaseController for ControllerA {
1354            fn middlewares(&self) -> Vec<String> {
1355                vec!["auth".to_string()]
1356            }
1357        }
1358
1359        impl SzController for ControllerB {}
1360        impl BaseController for ControllerB {
1361            fn middlewares(&self) -> Vec<String> {
1362                vec!["cors".to_string(), "log".to_string()]
1363            }
1364        }
1365
1366        let a = ControllerA;
1367        let b = ControllerB;
1368
1369        assert_eq!(a.middlewares(), vec!["auth"]);
1370        assert_eq!(b.middlewares(), vec!["cors", "log"]);
1371
1372        // 默认 batch_validate 不互相影响
1373        assert!(!a.batch_validate());
1374        assert!(!b.batch_validate());
1375    }
1376
1377    /// PHP 继承链验证:BaseController → SzController 方法可用性
1378    #[test]
1379    fn test_base_controller_inheritance_chain() {
1380        // 业务控制器实现 BaseController,自动获得 SzController 的所有方法
1381        struct BusinessController;
1382        impl SzController for BusinessController {}
1383        impl BaseController for BusinessController {}
1384
1385        let ctrl = BusinessController;
1386
1387        // SzController 方法(来自父 trait)
1388        let value = ctrl.render_json(0, "error", json!({}));
1389        assert!(value.is_object());
1390
1391        // BaseController 方法
1392        assert!(!ctrl.batch_validate());
1393        assert!(ctrl.middlewares().is_empty());
1394        ctrl.initialize();
1395    }
1396
1397    // ====================================================================
1398    // AddonsBaseController trait 测试
1399    // ====================================================================
1400
1401    /// 实现 AddonsBaseController 的 Mock 控制器(同时自动实现 BaseController + SzController)
1402    struct MockAddonsController;
1403
1404    impl SzController for MockAddonsController {}
1405    impl BaseController for MockAddonsController {}
1406    impl AddonsBaseController for MockAddonsController {}
1407
1408    #[test]
1409    fn test_addons_default_allow_all_action() {
1410        // PHP 默认 $allowAllAction = ['/passport/login', '/task/task/userClerk']
1411        let ctrl = MockAddonsController;
1412        let allow = ctrl.allow_all_action();
1413        assert!(allow.contains(&"/passport/login"));
1414        assert!(allow.contains(&"/task/task/userClerk"));
1415        assert_eq!(allow.len(), 2);
1416    }
1417
1418    #[test]
1419    fn test_addons_parse_route_info_two_segments() {
1420        // /passport/login → controller=passport, action=login, group=passport
1421        let ctrl = MockAddonsController;
1422        let info = ctrl.parse_route_info("/passport/login");
1423        assert_eq!(info.controller, "passport");
1424        assert_eq!(info.action, "login");
1425        assert_eq!(info.group, "passport");
1426        assert_eq!(info.route_uri, "/passport/login");
1427    }
1428
1429    #[test]
1430    fn test_addons_parse_route_info_three_segments() {
1431        // /task/task/userClerk → controller=task/task, action=userClerk
1432        // PHP bug 复刻:group=controller=task/task(详见 parse_route_info 文档注释)
1433        let ctrl = MockAddonsController;
1434        let info = ctrl.parse_route_info("/task/task/userClerk");
1435        assert_eq!(info.controller, "task/task");
1436        assert_eq!(info.action, "userClerk");
1437        assert_eq!(info.group, "task/task"); // PHP bug: group === controller
1438        assert_eq!(info.route_uri, "/task/task/userClerk");
1439    }
1440
1441    #[test]
1442    fn test_addons_parse_route_info_single_segment() {
1443        // /passport → controller=passport, action="", group=passport
1444        let ctrl = MockAddonsController;
1445        let info = ctrl.parse_route_info("/passport");
1446        assert_eq!(info.controller, "passport");
1447        assert_eq!(info.action, "");
1448        assert_eq!(info.group, "passport");
1449        assert_eq!(info.route_uri, "/passport/");
1450    }
1451
1452    #[test]
1453    fn test_addons_parse_route_info_root() {
1454        // / → 所有字段为空,route_uri="/"
1455        let ctrl = MockAddonsController;
1456        let info = ctrl.parse_route_info("/");
1457        assert_eq!(info.controller, "");
1458        assert_eq!(info.action, "");
1459        assert_eq!(info.group, "");
1460        assert_eq!(info.route_uri, "/");
1461    }
1462
1463    #[test]
1464    fn test_addons_parse_route_info_empty_uri() {
1465        let ctrl = MockAddonsController;
1466        let info = ctrl.parse_route_info("");
1467        assert_eq!(info.controller, "");
1468        assert_eq!(info.route_uri, "/");
1469    }
1470
1471    #[test]
1472    fn test_addons_parse_route_info_with_query_string() {
1473        // 含 query string 的 URI 应被剥离
1474        let ctrl = MockAddonsController;
1475        let info = ctrl.parse_route_info("/user/info?foo=bar&baz=1");
1476        assert_eq!(info.controller, "user");
1477        assert_eq!(info.action, "info");
1478        assert_eq!(info.route_uri, "/user/info");
1479    }
1480
1481    #[test]
1482    fn test_addons_parse_route_info_trailing_slash() {
1483        // 末尾斜杠
1484        let ctrl = MockAddonsController;
1485        let info = ctrl.parse_route_info("/user/info/");
1486        // split 会产生空字符串末尾段
1487        assert_eq!(info.controller, "user/info");
1488        assert_eq!(info.action, "");
1489        assert_eq!(info.route_uri, "/user/info/");
1490    }
1491
1492    #[test]
1493    fn test_addons_check_login_whitelist_pass() {
1494        // /passport/login 在白名单中,应通过
1495        let ctrl = MockAddonsController;
1496        let result = ctrl.check_login("/passport/login", false);
1497        assert!(result.is_ok());
1498    }
1499
1500    #[test]
1501    fn test_addons_check_login_whitelist_pass_2() {
1502        // /task/task/userClerk 在白名单中,应通过
1503        let ctrl = MockAddonsController;
1504        let result = ctrl.check_login("/task/task/userClerk", false);
1505        assert!(result.is_ok());
1506    }
1507
1508    #[test]
1509    fn test_addons_check_login_logged_in_pass() {
1510        // 不在白名单但已登录,应通过
1511        let ctrl = MockAddonsController;
1512        let result = ctrl.check_login("/user/info", true);
1513        assert!(result.is_ok());
1514    }
1515
1516    #[test]
1517    fn test_addons_check_login_not_logged_in_fail() {
1518        // 不在白名单且未登录,应返回 not_login
1519        let ctrl = MockAddonsController;
1520        let result = ctrl.check_login("/user/info", false);
1521        assert!(result.is_err());
1522        assert_eq!(result.unwrap_err(), "not_login");
1523    }
1524
1525    #[test]
1526    fn test_addons_get_token_default_returns_none() {
1527        // 未配置 SZ_JWT_SECRET 环境变量时,JWT_CONFIG 为 None,验证跳过返回 Ok(None)
1528        // 注:生产环境通过 `validate_jwt_config()` 在启动时校验,测试环境允许跳过
1529        let ctrl = MockAddonsController;
1530        let result = ctrl.get_token(Some("Bearer xxx.yyy.zzz"));
1531        assert!(result.is_ok());
1532        assert_eq!(result.unwrap(), None);
1533    }
1534
1535    #[test]
1536    fn test_addons_get_token_no_authorization() {
1537        let ctrl = MockAddonsController;
1538        let result = ctrl.get_token(None);
1539        assert!(result.is_ok());
1540        assert_eq!(result.unwrap(), None);
1541    }
1542
1543    #[test]
1544    fn test_addons_get_token_empty_authorization() {
1545        let ctrl = MockAddonsController;
1546        let result = ctrl.get_token(Some(""));
1547        assert!(result.is_ok());
1548        assert_eq!(result.unwrap(), None);
1549    }
1550
1551    #[test]
1552    fn test_addons_get_token_invalid_format_returns_none() {
1553        // 非法 JWT 格式(非三段式)应返回 Ok(None) 而非 Err
1554        // 注:仅在 SZ_JWT_SECRET 已设置时才会进入格式校验
1555        let ctrl = MockAddonsController;
1556        let result = ctrl.get_token(Some("Bearer not.a.valid.jwt.token"));
1557        assert!(result.is_ok());
1558        // 无论是否配置密钥,无效 token 都应返回 None
1559        assert_eq!(result.unwrap(), None);
1560    }
1561
1562    #[test]
1563    fn test_strip_bearer_prefix_uppercase() {
1564        assert_eq!(strip_bearer_prefix("Bearer abc.def.ghi"), "abc.def.ghi");
1565    }
1566
1567    #[test]
1568    fn test_strip_bearer_prefix_lowercase() {
1569        assert_eq!(strip_bearer_prefix("bearer abc.def.ghi"), "abc.def.ghi");
1570    }
1571
1572    #[test]
1573    fn test_strip_bearer_prefix_mixed_case() {
1574        assert_eq!(strip_bearer_prefix("BEARER abc.def.ghi"), "abc.def.ghi");
1575    }
1576
1577    #[test]
1578    fn test_strip_bearer_prefix_no_prefix() {
1579        // 无 Bearer 前缀时,应保持原样(去除首尾空格)
1580        assert_eq!(strip_bearer_prefix("abc.def.ghi"), "abc.def.ghi");
1581    }
1582
1583    #[test]
1584    fn test_strip_bearer_prefix_empty() {
1585        assert_eq!(strip_bearer_prefix(""), "");
1586    }
1587
1588    #[test]
1589    fn test_strip_bearer_prefix_with_extra_spaces() {
1590        assert_eq!(
1591            strip_bearer_prefix("  Bearer   abc.def.ghi  "),
1592            "abc.def.ghi"
1593        );
1594    }
1595
1596    /// JWT 端到端验证测试:使用真实 JwtEncoder 签发 token,再通过 verify_token_with_config 验证
1597    ///
1598    /// 通过 verify_token_with_config 注入测试配置,避免依赖全局环境变量,
1599    /// 确保 CI 环境也能完整执行 JWT 验证流程测试。
1600    #[test]
1601    fn test_get_token_valid_jwt_returns_user_info() {
1602        let config = JwtConfig {
1603            secret: "test-secret".to_string(),
1604            issuer: String::new(), // 不验证 iss,
1605            audience: String::new(),
1606        };
1607
1608        // 签发一个有效 token
1609        let encoder = sz_orm_auth::jwt::JwtEncoder::new(&config.secret);
1610        let exp = std::time::SystemTime::now()
1611            .duration_since(std::time::UNIX_EPOCH)
1612            .unwrap()
1613            .as_secs() as i64
1614            + 3600; // 1 小时后过期
1615        let claims = sz_orm_auth::jwt::JwtClaims::new("user123", exp).with_user_id(12345);
1616        let token = encoder.encode(&claims).unwrap();
1617
1618        let result = verify_token_with_config(Some(&format!("Bearer {token}")), &config);
1619        assert!(result.is_ok());
1620        let user = result.unwrap();
1621        assert!(user.is_some());
1622        let user = user.unwrap();
1623        assert_eq!(user.user_id, 12345);
1624        assert!(user.is_login);
1625    }
1626
1627    /// 测试 JWT 签名密钥错误时返回 None
1628    #[test]
1629    fn test_get_token_wrong_secret_returns_none() {
1630        let config = JwtConfig {
1631            secret: "correct-secret".to_string(),
1632            issuer: String::new(),
1633            audience: String::new(),
1634        };
1635
1636        // 用错误密钥签发 token
1637        let encoder = sz_orm_auth::jwt::JwtEncoder::new("wrong-secret");
1638        let exp = std::time::SystemTime::now()
1639            .duration_since(std::time::UNIX_EPOCH)
1640            .unwrap()
1641            .as_secs() as i64
1642            + 3600;
1643        let claims = sz_orm_auth::jwt::JwtClaims::new("user123", exp).with_user_id(12345);
1644        let token = encoder.encode(&claims).unwrap();
1645
1646        let result = verify_token_with_config(Some(&format!("Bearer {token}")), &config);
1647        assert!(result.is_ok());
1648        assert_eq!(result.unwrap(), None); // 签名验证失败
1649    }
1650
1651    /// 测试 JWT 过期时返回 None
1652    #[test]
1653    fn test_get_token_expired_returns_none() {
1654        let config = JwtConfig {
1655            secret: "test-secret".to_string(),
1656            issuer: String::new(),
1657            audience: String::new(),
1658        };
1659
1660        let encoder = sz_orm_auth::jwt::JwtEncoder::new(&config.secret);
1661        // 过期时间为 1 小时前
1662        let exp = std::time::SystemTime::now()
1663            .duration_since(std::time::UNIX_EPOCH)
1664            .unwrap()
1665            .as_secs() as i64
1666            - 3600;
1667        let claims = sz_orm_auth::jwt::JwtClaims::new("user123", exp).with_user_id(12345);
1668        let token = encoder.encode(&claims).unwrap();
1669
1670        let result = verify_token_with_config(Some(&format!("Bearer {token}")), &config);
1671        assert!(result.is_ok());
1672        assert_eq!(result.unwrap(), None); // 已过期
1673    }
1674
1675    /// 测试 JWT 缺少 user_id claim 时返回 None(兼容旧版 token)
1676    #[test]
1677    fn test_get_token_no_user_id_claim_returns_none() {
1678        let config = JwtConfig {
1679            secret: "test-secret".to_string(),
1680            issuer: String::new(),
1681            audience: String::new(),
1682        };
1683
1684        let encoder = sz_orm_auth::jwt::JwtEncoder::new(&config.secret);
1685        let exp = std::time::SystemTime::now()
1686            .duration_since(std::time::UNIX_EPOCH)
1687            .unwrap()
1688            .as_secs() as i64
1689            + 3600;
1690        // 不调用 with_user_id,user_id 为 None
1691        let claims = sz_orm_auth::jwt::JwtClaims::new("user123", exp);
1692        let token = encoder.encode(&claims).unwrap();
1693
1694        let result = verify_token_with_config(Some(&format!("Bearer {token}")), &config);
1695        assert!(result.is_ok());
1696        assert_eq!(result.unwrap(), None); // 缺少 user_id
1697    }
1698
1699    /// 测试 iss 验证:iss 不匹配时返回 None
1700    #[test]
1701    fn test_get_token_iss_mismatch_returns_none() {
1702        let config = JwtConfig {
1703            secret: "test-secret".to_string(),
1704            issuer: "https://expected-issuer.com".to_string(),
1705            audience: String::new(),
1706        };
1707
1708        let encoder = sz_orm_auth::jwt::JwtEncoder::new(&config.secret);
1709        let exp = std::time::SystemTime::now()
1710            .duration_since(std::time::UNIX_EPOCH)
1711            .unwrap()
1712            .as_secs() as i64
1713            + 3600;
1714        // 使用错误的 issuer 签发
1715        let claims = sz_orm_auth::jwt::JwtClaims::new("user123", exp)
1716            .with_issuer("https://wrong-issuer.com")
1717            .with_user_id(12345);
1718        let token = encoder.encode(&claims).unwrap();
1719
1720        let result = verify_token_with_config(Some(&format!("Bearer {token}")), &config);
1721        assert!(result.is_ok());
1722        assert_eq!(result.unwrap(), None); // iss 不匹配
1723    }
1724
1725    /// 测试 iss 验证:iss 匹配时返回 UserInfo
1726    #[test]
1727    fn test_get_token_iss_match_returns_user_info() {
1728        let config = JwtConfig {
1729            secret: "test-secret".to_string(),
1730            issuer: "https://mall.ljclz.shop".to_string(),
1731            audience: String::new(),
1732        };
1733
1734        let encoder = sz_orm_auth::jwt::JwtEncoder::new(&config.secret);
1735        let exp = std::time::SystemTime::now()
1736            .duration_since(std::time::UNIX_EPOCH)
1737            .unwrap()
1738            .as_secs() as i64
1739            + 3600;
1740        let claims = sz_orm_auth::jwt::JwtClaims::new("user123", exp)
1741            .with_issuer(&config.issuer)
1742            .with_user_id(67890);
1743        let token = encoder.encode(&claims).unwrap();
1744
1745        let result = verify_token_with_config(Some(&format!("Bearer {token}")), &config);
1746        assert!(result.is_ok());
1747        let user = result.unwrap().unwrap();
1748        assert_eq!(user.user_id, 67890);
1749        assert!(user.is_login);
1750    }
1751
1752    /// 测试密钥未配置时返回 None(禁用 JWT 验证)
1753    #[test]
1754    fn test_get_token_empty_secret_returns_none() {
1755        let config = JwtConfig::default(); // secret 为空
1756
1757        let result = verify_token_with_config(Some("Bearer any.token.here"), &config);
1758        assert!(result.is_ok());
1759        assert_eq!(result.unwrap(), None); // 密钥未配置
1760    }
1761
1762    /// 测试无 Bearer 前缀的 token 也能正常解析
1763    #[test]
1764    fn test_get_token_without_bearer_prefix() {
1765        let config = JwtConfig {
1766            secret: "test-secret".to_string(),
1767            issuer: String::new(),
1768            audience: String::new(),
1769        };
1770
1771        let encoder = sz_orm_auth::jwt::JwtEncoder::new(&config.secret);
1772        let exp = std::time::SystemTime::now()
1773            .duration_since(std::time::UNIX_EPOCH)
1774            .unwrap()
1775            .as_secs() as i64
1776            + 3600;
1777        let claims = sz_orm_auth::jwt::JwtClaims::new("user123", exp).with_user_id(99999);
1778        let token = encoder.encode(&claims).unwrap();
1779
1780        // 不带 Bearer 前缀,应仍能解析(对齐 PHP str_ireplace 兼容行为)
1781        let result = verify_token_with_config(Some(&token), &config);
1782        assert!(result.is_ok());
1783        let user = result.unwrap().unwrap();
1784        assert_eq!(user.user_id, 99999);
1785    }
1786
1787    /// 覆盖白名单的子类控制器
1788    struct CustomAddonsController;
1789
1790    impl SzController for CustomAddonsController {}
1791    impl BaseController for CustomAddonsController {}
1792    impl AddonsBaseController for CustomAddonsController {
1793        fn allow_all_action(&self) -> Vec<&'static str> {
1794            vec!["/custom/public", "/custom/health"]
1795        }
1796    }
1797
1798    #[test]
1799    fn test_addons_override_allow_all_action() {
1800        let ctrl = CustomAddonsController;
1801        let allow = ctrl.allow_all_action();
1802        assert_eq!(allow, vec!["/custom/public", "/custom/health"]);
1803        // 不应包含默认白名单
1804        assert!(!allow.contains(&"/passport/login"));
1805    }
1806
1807    #[test]
1808    fn test_addons_override_check_login_uses_new_whitelist() {
1809        let ctrl = CustomAddonsController;
1810        // /custom/public 在新白名单中
1811        assert!(ctrl.check_login("/custom/public", false).is_ok());
1812        // /passport/login 不在新白名单中(被覆盖)
1813        assert!(ctrl.check_login("/passport/login", false).is_err());
1814    }
1815
1816    /// 模拟 PHP addons 控制器典型流程:parse_route_info → get_token → check_login → renderSuccess
1817    #[tokio::test]
1818    async fn test_addons_php_full_flow_whitelist() {
1819        struct PassportController;
1820        impl SzController for PassportController {}
1821        impl BaseController for PassportController {}
1822        impl AddonsBaseController for PassportController {}
1823
1824        let ctrl = PassportController;
1825
1826        // 1. 解析路由信息
1827        let info = ctrl.parse_route_info("/passport/login");
1828        assert_eq!(info.route_uri, "/passport/login");
1829
1830        // 2. 获取 token(占位返回 None)
1831        let user = ctrl.get_token(None).unwrap();
1832
1833        // 3. 检查登录(白名单通过,user_is_login 不影响)
1834        let is_login = user.as_ref().is_some_and(|u| u.is_login);
1835        ctrl.check_login(&info.route_uri, is_login).unwrap();
1836
1837        // 4. 返回成功响应(模拟登录成功)
1838        let resp = ctrl.render_success("登录成功", json!({"token": "fake.jwt.token"}));
1839        let body = collect_body(resp).await;
1840        assert_eq!(
1841            body,
1842            r#"{"code":1,"msg":"登录成功","data":{"token":"fake.jwt.token"}}"#
1843        );
1844    }
1845
1846    #[tokio::test]
1847    async fn test_addons_php_full_flow_not_login() {
1848        struct UserController;
1849        impl SzController for UserController {}
1850        impl BaseController for UserController {}
1851        impl AddonsBaseController for UserController {}
1852
1853        let ctrl = UserController;
1854
1855        // 1. 解析路由信息
1856        let info = ctrl.parse_route_info("/user/info");
1857
1858        // 2. 获取 token(占位返回 None,即未登录)
1859        let user = ctrl.get_token(None).unwrap();
1860        let is_login = user.as_ref().is_some_and(|u| u.is_login);
1861
1862        // 3. 检查登录(白名单不通过,且未登录)
1863        let result = ctrl.check_login(&info.route_uri, is_login);
1864        assert!(result.is_err());
1865        assert_eq!(result.unwrap_err(), "not_login");
1866
1867        // 4. 返回未登录响应(模拟 PHP BaseException ['code' => -1, 'msg' => 'not_login'])
1868        let resp = ctrl.render_error("not_login", json!({}), -1);
1869        let body = collect_body(resp).await;
1870        assert_eq!(body, r#"{"code":-1,"msg":"not_login","data":{}}"#);
1871    }
1872
1873    #[tokio::test]
1874    async fn test_addons_php_full_flow_logged_in() {
1875        struct OrderController;
1876        impl SzController for OrderController {}
1877        impl BaseController for OrderController {}
1878        impl AddonsBaseController for OrderController {
1879            // 模拟已登录用户(覆盖 get_token 返回 UserInfo)
1880            fn get_token(&self, _authorization: Option<&str>) -> Result<Option<UserInfo>, String> {
1881                Ok(Some(UserInfo {
1882                    user_id: 12345,
1883                    is_login: true,
1884                }))
1885            }
1886        }
1887
1888        let ctrl = OrderController;
1889
1890        // 1. 解析路由信息
1891        let info = ctrl.parse_route_info("/order/list");
1892
1893        // 2. 获取 token(返回已登录用户)
1894        let user = ctrl.get_token(None).unwrap();
1895        let is_login = user.as_ref().is_some_and(|u| u.is_login);
1896
1897        // 3. 检查登录(白名单不通过,但已登录)
1898        ctrl.check_login(&info.route_uri, is_login).unwrap();
1899
1900        // 4. 返回订单列表
1901        let resp = ctrl.render_success(
1902            "success",
1903            json!({"list": [{"id": 1}, {"id": 2}], "total": 2}),
1904        );
1905        let body = collect_body(resp).await;
1906        assert_eq!(
1907            body,
1908            r#"{"code":1,"msg":"success","data":{"list":[{"id":1},{"id":2}],"total":2}}"#
1909        );
1910    }
1911
1912    /// 测试 RouteInfo / UserInfo 类型
1913    #[test]
1914    fn test_route_info_debug_clone_eq() {
1915        let info1 = RouteInfo {
1916            controller: "user".to_string(),
1917            action: "info".to_string(),
1918            route_uri: "/user/info".to_string(),
1919            group: "user".to_string(),
1920        };
1921        let info2 = info1.clone();
1922        assert_eq!(info1, info2);
1923        let debug_str = format!("{info1:?}");
1924        assert!(debug_str.contains("RouteInfo"));
1925        assert!(debug_str.contains("user"));
1926    }
1927
1928    #[test]
1929    fn test_user_info_debug_clone_eq() {
1930        let user1 = UserInfo {
1931            user_id: 12345,
1932            is_login: true,
1933        };
1934        let user2 = user1.clone();
1935        assert_eq!(user1, user2);
1936        let debug_str = format!("{user1:?}");
1937        assert!(debug_str.contains("UserInfo"));
1938        assert!(debug_str.contains("12345"));
1939    }
1940
1941    /// 继承链验证:AddonsBaseController → BaseController → SzController
1942    #[test]
1943    fn test_addons_inheritance_chain_all_methods() {
1944        struct BusinessController;
1945        impl SzController for BusinessController {}
1946        impl BaseController for BusinessController {}
1947        impl AddonsBaseController for BusinessController {}
1948
1949        let ctrl = BusinessController;
1950
1951        // SzController 方法(祖父 trait)
1952        let value = ctrl.render_json(1, "ok", json!({}));
1953        assert_eq!(value["code"], 1);
1954
1955        // BaseController 方法(父 trait)
1956        assert!(!ctrl.batch_validate());
1957        assert!(ctrl.middlewares().is_empty());
1958        ctrl.initialize();
1959
1960        // AddonsBaseController 方法(当前 trait)
1961        let info = ctrl.parse_route_info("/test/action");
1962        assert_eq!(info.route_uri, "/test/action");
1963        assert!(ctrl.check_login("/passport/login", false).is_ok());
1964        assert!(ctrl.get_token(None).unwrap().is_none());
1965    }
1966
1967    // ========================================================================
1968    // P0-SEC-02:JWT 密钥空字符串处理(认证绕过防护)
1969    // ========================================================================
1970
1971    /// P0-SEC-02 回归测试:空密钥配置下 verify_token_with_config 必须返回 Ok(None)
1972    ///
1973    /// 安全铁律:空密钥 = 无密钥 = 禁用 JWT 验证。绝不能以空密钥尝试解码 token
1974    /// (某些 JWT 库在空密钥下会接受 alg=none 的伪造 token,导致认证绕过)。
1975    #[test]
1976    fn test_p0_sec_02_empty_secret_returns_none_not_accept_token() {
1977        let config = JwtConfig {
1978            secret: String::new(), // 空密钥
1979            issuer: String::new(),
1980            audience: String::new(),
1981        };
1982
1983        // 即使传入看似有效的 token,空密钥下也必须拒绝验证(返回 None)
1984        let result = verify_token_with_config(Some("any.token.here"), &config);
1985        assert!(
1986            result.is_ok(),
1987            "空密钥不应导致 panic 或 Err(应优雅降级为 None)"
1988        );
1989        assert_eq!(
1990            result.unwrap(),
1991            None,
1992            "P0-SEC-02: 空密钥时必须返回 None,绝不能接受任何 token"
1993        );
1994    }
1995
1996    /// P0-SEC-02 回归测试:空密钥下即使传入 alg=none 的伪造 token 也必须拒绝
1997    ///
1998    /// alg=none 攻击:某些 JWT 实现在密钥为空时会接受 header 中 alg=none 的 token。
1999    /// 此测试确保我们的实现不会落入此陷阱。
2000    #[test]
2001    fn test_p0_sec_02_rejects_alg_none_token_with_empty_secret() {
2002        let config = JwtConfig::default(); // 空密钥
2003
2004        // 构造一个 alg=none 的伪造 JWT(header: {"alg":"none","typ":"JWT"})
2005        // base64url({"alg":"none","typ":"JWT"}) = eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0
2006        // base64url({"user_id":1,"username":"hacker","iat":0,"exp":9999999999})
2007        //   = eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImhhY2tlciIsImlhdCI6MCwiZXhwIjo5OTk5OTk5OTk5fQ
2008        let alg_none_token = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.\
2009            eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImhhY2tlciIsImlhdCI6MCwiZXhwIjo5OTk5OTk5OTk5fQ.";
2010
2011        let result = verify_token_with_config(Some(&alg_none_token), &config);
2012        assert!(
2013            result.is_ok(),
2014            "空密钥下不应 panic"
2015        );
2016        assert_eq!(
2017            result.unwrap(),
2018            None,
2019            "P0-SEC-02: 空密钥下 alg=none 伪造 token 必须被拒绝(返回 None)"
2020        );
2021    }
2022
2023    // ========================================================================
2024    // P1-SEC-10:JWT Audience (aud) 验证
2025    // ========================================================================
2026
2027    /// P1-SEC-10 回归测试:配置了 audience 时,token 的 aud 不匹配应拒绝
2028    #[test]
2029    fn test_p1_sec_10_rejects_token_with_wrong_audience() {
2030        let config = JwtConfig {
2031            secret: "test-secret".to_string(),
2032            issuer: String::new(),
2033            audience: "https://my-app.com".to_string(),
2034        };
2035
2036        let encoder = sz_orm_auth::jwt::JwtEncoder::new(&config.secret);
2037        let exp = std::time::SystemTime::now()
2038            .duration_since(std::time::UNIX_EPOCH)
2039            .unwrap()
2040            .as_secs() as i64
2041            + 3600;
2042        let claims = sz_orm_auth::jwt::JwtClaims::new("user123", exp)
2043            .with_user_id(42)
2044            .with_audience("https://other-app.com");
2045        let token = encoder.encode(&claims).unwrap();
2046
2047        let result = verify_token_with_config(Some(&token), &config);
2048        assert!(result.is_ok());
2049        assert_eq!(result.unwrap(), None, "P1-SEC-10: aud 不匹配的 token 必须被拒绝");
2050    }
2051
2052    /// P1-SEC-10 回归测试:配置了 audience 时,token 的 aud 匹配应通过
2053    #[test]
2054    fn test_p1_sec_10_accepts_token_with_matching_audience() {
2055        let config = JwtConfig {
2056            secret: "test-secret".to_string(),
2057            issuer: String::new(),
2058            audience: "https://my-app.com".to_string(),
2059        };
2060
2061        let encoder = sz_orm_auth::jwt::JwtEncoder::new(&config.secret);
2062        let exp = std::time::SystemTime::now()
2063            .duration_since(std::time::UNIX_EPOCH)
2064            .unwrap()
2065            .as_secs() as i64
2066            + 3600;
2067        let claims = sz_orm_auth::jwt::JwtClaims::new("user123", exp)
2068            .with_user_id(42)
2069            .with_audience("https://my-app.com");
2070        let token = encoder.encode(&claims).unwrap();
2071
2072        let result = verify_token_with_config(Some(&token), &config);
2073        assert!(result.is_ok());
2074        assert!(result.unwrap().is_some(), "aud 匹配的 token 应通过验证");
2075    }
2076
2077    /// P1-SEC-10 回归测试:未配置 audience 时,跳过 aud 验证(向后兼容)
2078    #[test]
2079    fn test_p1_sec_10_skips_audience_check_when_not_configured() {
2080        let config = JwtConfig {
2081            secret: "test-secret".to_string(),
2082            issuer: String::new(),
2083            audience: String::new(),
2084        };
2085
2086        let encoder = sz_orm_auth::jwt::JwtEncoder::new(&config.secret);
2087        let exp = std::time::SystemTime::now()
2088            .duration_since(std::time::UNIX_EPOCH)
2089            .unwrap()
2090            .as_secs() as i64
2091            + 3600;
2092        let claims = sz_orm_auth::jwt::JwtClaims::new("user123", exp)
2093            .with_user_id(42);
2094        let token = encoder.encode(&claims).unwrap();
2095
2096        let result = verify_token_with_config(Some(&token), &config);
2097        assert!(result.is_ok());
2098        assert!(result.unwrap().is_some(), "未配置 audience 时应跳过 aud 验证");
2099    }
2100
2101    // ========================================================================
2102    // P1-SEC-12:JwtConfig Debug 实现不泄漏 secret
2103    // ========================================================================
2104
2105    /// P1-SEC-12 回归测试:JwtConfig 的 Debug 输出必须脱敏 secret 字段
2106    ///
2107    /// 若 JwtConfig 使用派生 Debug,secret 字段会以明文出现在日志和 panic 信息中。
2108    /// 手动实现的 Debug 应将 secret 替换为 "[REDACTED]"。
2109    #[test]
2110    fn test_p1_sec_12_jwt_config_debug_redacts_secret() {
2111        let config = JwtConfig {
2112            secret: "super-secret-key-12345".to_string(),
2113            issuer: "https://example.com".to_string(),
2114            audience: String::new(),
2115        };
2116
2117        let debug_output = format!("{:?}", config);
2118
2119        // secret 绝不能出现在 Debug 输出中
2120        assert!(
2121            !debug_output.contains("super-secret-key-12345"),
2122            "P1-SEC-12: JwtConfig::Debug 泄漏了 secret 字段: {debug_output}"
2123        );
2124        // 必须包含脱敏标记
2125        assert!(
2126            debug_output.contains("[REDACTED]"),
2127            "P1-SEC-12: JwtConfig::Debug 应包含 [REDACTED] 标记: {debug_output}"
2128        );
2129        // issuer 可以正常显示
2130        assert!(
2131            debug_output.contains("https://example.com"),
2132            "issuer 字段应正常显示: {debug_output}"
2133        );
2134    }
2135}