use axum::body::Body;
use axum::http::Request;
use axum::response::{IntoResponse, Response};
use indexmap::IndexMap;
use once_cell::sync::Lazy;
use serde_json::{Map, Value};
use std::future::Future;
use crate::request::{
fetch_post_data, fetch_post_data_by_key, fetch_query_data, fetch_query_data_by_key,
};
use crate::response::ApiResponse;
use crate::validate::Validate;
#[derive(Debug, Clone, Default)]
struct JwtConfig {
secret: String,
issuer: String,
}
static JWT_CONFIG: Lazy<JwtConfig> = Lazy::new(|| JwtConfig {
secret: std::env::var("SZ_JWT_SECRET").unwrap_or_default(),
issuer: std::env::var("SZ_JWT_ISSUER").unwrap_or_default(),
});
fn strip_bearer_prefix(header: &str) -> &str {
let trimmed = header.trim();
if trimmed.len() >= 6 {
let prefix = &trimmed[..6];
if prefix.eq_ignore_ascii_case("bearer") {
return trimmed[6..].trim_start();
}
}
trimmed
}
fn verify_token_with_config(
authorization: Option<&str>,
config: &JwtConfig,
) -> Result<Option<UserInfo>, String> {
let header_value = match authorization {
Some(v) if !v.is_empty() => v,
_ => return Ok(None),
};
let token = strip_bearer_prefix(header_value).trim();
if token.is_empty() {
return Ok(None);
}
if config.secret.is_empty() {
return Ok(None);
}
let encoder = sz_orm_auth::jwt::JwtEncoder::new(&config.secret);
let claims = match encoder.decode(token) {
Ok(c) => c,
Err(_) => return Ok(None),
};
if !config.issuer.is_empty() {
match &claims.iss {
Some(iss) if iss == &config.issuer => { }
_ => return Ok(None),
}
}
let user_id = match claims.user_id {
Some(id) => id,
None => return Ok(None),
};
Ok(Some(UserInfo {
user_id,
is_login: true,
}))
}
pub trait SzController: Send + Sync {
fn render_json(&self, code: i32, msg: impl Into<String>, data: Value) -> Value {
let mut map = Map::new();
map.insert("code".to_string(), Value::Number(code.into()));
map.insert("msg".to_string(), Value::String(msg.into()));
map.insert("data".to_string(), data);
Value::Object(map)
}
fn render_success(&self, msg: impl Into<String>, data: Value) -> Response {
ApiResponse::success(data, msg).into_response()
}
fn render_error(&self, msg: impl Into<String>, data: Value, code: i32) -> Response {
ApiResponse::error_with_code(code, msg, data).into_response()
}
fn post_data(&self, req: Request<Body>) -> impl Future<Output = Result<Value, String>> + Send {
async move { fetch_post_data(req).await }
}
fn post_data_by_key(
&self,
req: Request<Body>,
key: &str,
) -> impl Future<Output = Result<Option<Value>, String>> + Send {
async move { fetch_post_data_by_key(req, key).await }
}
fn get_data(&self, req: &Request<Body>) -> Value {
fetch_query_data(req)
}
fn get_data_by_key(&self, req: &Request<Body>, key: &str) -> Option<Value> {
fetch_query_data_by_key(req, key)
}
}
pub trait BaseController: SzController {
fn batch_validate(&self) -> bool {
false
}
fn middlewares(&self) -> Vec<String> {
Vec::new()
}
fn initialize(&self) {}
fn validate(
&self,
data: &Value,
rules: &[(&str, &str)],
messages: &[(&str, &str)],
) -> Result<(), String> {
let mut validator = Validate::new();
for (name, rule) in rules {
validator = validator.rule(name, rule);
}
let mut msg_map = IndexMap::new();
for (key, msg) in messages {
msg_map.insert(key.to_string(), msg.to_string());
}
validator = validator.message(msg_map);
if self.batch_validate() {
validator = validator.batch(true);
}
match validator.check(data) {
Ok(()) => Ok(()),
Err(err) => Err(err.to_string()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RouteInfo {
pub controller: String,
pub action: String,
pub route_uri: String,
pub group: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UserInfo {
pub user_id: i64,
pub is_login: bool,
}
pub trait AddonsBaseController: BaseController {
fn allow_all_action(&self) -> Vec<&'static str> {
vec!["/passport/login", "/task/task/userClerk"]
}
fn parse_route_info(&self, uri: &str) -> RouteInfo {
let path = uri.split('?').next().unwrap_or("");
let path = path.trim_start_matches('/');
let segments: Vec<&str> = if path.is_empty() {
Vec::new()
} else {
path.split('/').collect()
};
let (controller, action) = match segments.len() {
0 => (String::new(), String::new()),
1 => (segments[0].to_string(), String::new()),
_ => (
segments[..segments.len() - 1].join("/"),
segments[segments.len() - 1].to_string(),
),
};
let group = controller.clone();
let route_uri = if controller.is_empty() && action.is_empty() {
"/".to_string()
} else {
format!("/{controller}/{action}")
};
RouteInfo {
controller,
action,
route_uri,
group,
}
}
fn check_login(&self, route_uri: &str, user_is_login: bool) -> Result<(), String> {
if self.allow_all_action().contains(&route_uri) {
return Ok(());
}
if user_is_login {
return Ok(());
}
Err("not_login".to_string())
}
fn get_token(&self, authorization: Option<&str>) -> Result<Option<UserInfo>, String> {
verify_token_with_config(authorization, &JWT_CONFIG)
}
}
#[cfg(test)]
mod tests {
use super::*;
use axum::body::Body;
use axum::http::{Method, Request, StatusCode};
use http_body_util::BodyExt;
use serde_json::json;
use tower::ServiceExt;
struct MockController;
impl SzController for MockController {}
fn make_json_request(body: &str, query: Option<&str>) -> Request<Body> {
let uri = match query {
Some(q) => format!("/?{q}"),
None => "/".to_string(),
};
Request::builder()
.method(Method::POST)
.uri(&uri)
.header("content-type", "application/json")
.body(Body::from(body.to_string()))
.unwrap()
}
fn make_get_request(query: &str) -> Request<Body> {
Request::builder()
.method(Method::GET)
.uri(format!("/?{query}"))
.body(Body::empty())
.unwrap()
}
async fn collect_body(resp: Response) -> String {
let bytes = resp.into_body().collect().await.unwrap().to_bytes();
String::from_utf8(bytes.to_vec()).unwrap()
}
#[test]
fn test_render_json_field_order() {
let ctrl = MockController;
let value = ctrl.render_json(1, "ok", json!({"id": 1}));
let obj = value.as_object().unwrap();
let keys: Vec<&String> = obj.keys().collect();
assert_eq!(keys, vec!["code", "msg", "data"]);
}
#[test]
fn test_render_json_default_values() {
let ctrl = MockController;
let value = ctrl.render_json(1, "", Value::Object(Map::new()));
assert_eq!(value["code"], 1);
assert_eq!(value["msg"], "");
assert!(value["data"].is_object());
assert!(value["data"].as_object().unwrap().is_empty());
}
#[test]
fn test_render_json_custom_code() {
let ctrl = MockController;
let value = ctrl.render_json(-1, "未登录", json!({}));
assert_eq!(value["code"], -1);
assert_eq!(value["msg"], "未登录");
}
#[test]
fn test_render_json_compact_semantics() {
let ctrl = MockController;
let value = ctrl.render_json(0, "失败", json!({"field": "name"}));
let json_str = value.to_string();
assert_eq!(
json_str,
r#"{"code":0,"msg":"失败","data":{"field":"name"}}"#
);
}
#[test]
fn test_render_json_returns_value_not_response() {
let ctrl = MockController;
let value = ctrl.render_json(1, "ok", json!({}));
assert!(value.is_object());
}
#[test]
fn test_render_success_returns_response() {
let ctrl = MockController;
let resp = ctrl.render_success("success", json!({"id": 1}));
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers().get("content-type").unwrap(),
"application/json; charset=utf-8"
);
}
#[tokio::test]
async fn test_render_success_body_format() {
let ctrl = MockController;
let resp = ctrl.render_success("success", json!({"id": 1, "name": "alice"}));
let body = collect_body(resp).await;
assert_eq!(
body,
r#"{"code":1,"msg":"success","data":{"id":1,"name":"alice"}}"#
);
}
#[tokio::test]
async fn test_render_success_default_msg() {
let ctrl = MockController;
let resp = ctrl.render_success("success", json!({}));
let body = collect_body(resp).await;
assert_eq!(body, r#"{"code":1,"msg":"success","data":{}}"#);
}
#[tokio::test]
async fn test_render_success_via_axum_router() {
struct UserController;
impl SzController for UserController {}
async fn handler() -> Response {
let ctrl = UserController;
ctrl.render_success("ok", json!({"id": 1}))
}
let router = axum::Router::new().route("/", axum::routing::get(handler));
let req = Request::builder()
.method(Method::GET)
.uri("/")
.body(Body::empty())
.unwrap();
let resp = router.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = collect_body(resp).await;
assert_eq!(body, r#"{"code":1,"msg":"ok","data":{"id":1}}"#);
}
#[test]
fn test_render_error_returns_response() {
let ctrl = MockController;
let resp = ctrl.render_error("error", json!({}), 0);
assert_eq!(resp.status(), StatusCode::OK); }
#[tokio::test]
async fn test_render_error_default_code() {
let ctrl = MockController;
let resp = ctrl.render_error("参数错误", json!({}), 0);
let body = collect_body(resp).await;
assert_eq!(body, r#"{"code":0,"msg":"参数错误","data":{}}"#);
}
#[tokio::test]
async fn test_render_error_custom_code() {
let ctrl = MockController;
let resp = ctrl.render_error("not_login", json!({}), -1);
let body = collect_body(resp).await;
assert_eq!(body, r#"{"code":-1,"msg":"not_login","data":{}}"#);
}
#[tokio::test]
async fn test_render_error_with_data() {
let ctrl = MockController;
let resp = ctrl.render_error("失败", json!({"field": "name"}), 0);
let body = collect_body(resp).await;
assert_eq!(body, r#"{"code":0,"msg":"失败","data":{"field":"name"}}"#);
}
#[tokio::test]
async fn test_render_error_via_axum_router() {
struct UserController;
impl SzController for UserController {}
async fn handler() -> Response {
let ctrl = UserController;
ctrl.render_error("参数错误", json!({}), 0)
}
let router = axum::Router::new().route("/", axum::routing::post(handler));
let req = Request::builder()
.method(Method::POST)
.uri("/")
.body(Body::empty())
.unwrap();
let resp = router.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = collect_body(resp).await;
assert_eq!(body, r#"{"code":0,"msg":"参数错误","data":{}}"#);
}
#[tokio::test]
async fn test_post_data_json_body() {
let ctrl = MockController;
let req = make_json_request(r#"{"name":"alice","age":30}"#, None);
let data = ctrl.post_data(req).await.unwrap();
assert_eq!(data["name"], "alice");
assert_eq!(data["age"], 30);
}
#[tokio::test]
async fn test_post_data_query_only() {
let ctrl = MockController;
let req = make_json_request("", Some("page=1&size=10"));
let data = ctrl.post_data(req).await.unwrap();
assert_eq!(data["page"], "1");
assert_eq!(data["size"], "10");
}
#[tokio::test]
async fn test_post_data_body_overrides_query() {
let ctrl = MockController;
let req = make_json_request(r#"{"page":99}"#, Some("page=1&size=10"));
let data = ctrl.post_data(req).await.unwrap();
assert_eq!(data["page"], 99);
assert_eq!(data["size"], "10");
}
#[tokio::test]
async fn test_post_data_by_key_exists() {
let ctrl = MockController;
let req = make_json_request(r#"{"name":"alice","age":30}"#, None);
let name = ctrl.post_data_by_key(req, "name").await.unwrap();
assert_eq!(name, Some(json!("alice")));
}
#[tokio::test]
async fn test_post_data_by_key_missing() {
let ctrl = MockController;
let req = make_json_request(r#"{"name":"alice"}"#, None);
let age = ctrl.post_data_by_key(req, "age").await.unwrap();
assert_eq!(age, None);
}
#[test]
fn test_get_data_query() {
let ctrl = MockController;
let req = make_get_request("page=1&size=10");
let data = ctrl.get_data(&req);
assert_eq!(data["page"], "1");
assert_eq!(data["size"], "10");
}
#[test]
fn test_get_data_empty_query() {
let ctrl = MockController;
let req = make_get_request("");
let data = ctrl.get_data(&req);
assert!(data.as_object().unwrap().is_empty());
}
#[test]
fn test_get_data_by_key_exists() {
let ctrl = MockController;
let req = make_get_request("page=1&size=10");
let page = ctrl.get_data_by_key(&req, "page");
assert_eq!(page, Some(json!("1")));
}
#[test]
fn test_get_data_by_key_missing() {
let ctrl = MockController;
let req = make_get_request("page=1");
let size = ctrl.get_data_by_key(&req, "size");
assert_eq!(size, None);
}
#[tokio::test]
async fn test_multiple_controllers_independent() {
struct CtrlA;
struct CtrlB;
impl SzController for CtrlA {}
impl SzController for CtrlB {}
let a = CtrlA;
let b = CtrlB;
let req_a = make_json_request(r#"{"k":"a"}"#, None);
let req_b = make_json_request(r#"{"k":"b"}"#, None);
let data_a = a.post_data(req_a).await.unwrap();
let data_b = b.post_data(req_b).await.unwrap();
assert_eq!(data_a["k"], "a");
assert_eq!(data_b["k"], "b");
let va = a.render_json(1, "a", json!({}));
let vb = b.render_json(0, "b", json!({}));
assert_eq!(va["code"], 1);
assert_eq!(va["msg"], "a");
assert_eq!(vb["code"], 0);
assert_eq!(vb["msg"], "b");
}
#[tokio::test]
async fn test_php_consistency_full_flow() {
struct OrderController;
impl SzController for OrderController {}
let ctrl = OrderController;
let req = make_json_request(r#"{"order_id":12345,"amount":99.5}"#, None);
let data = ctrl.post_data(req).await.unwrap();
let order_id = data["order_id"].as_i64().unwrap();
let amount = data["amount"].as_f64().unwrap();
let result = json!({
"order_id": order_id,
"amount": amount,
"status": "paid"
});
let resp = ctrl.render_success("支付成功", result);
let body = collect_body(resp).await;
assert_eq!(
body,
r#"{"code":1,"msg":"支付成功","data":{"order_id":12345,"amount":99.5,"status":"paid"}}"#
);
}
#[tokio::test]
async fn test_php_consistency_error_flow() {
struct UserController;
impl SzController for UserController {}
let ctrl = UserController;
let req = make_json_request(r#"{"name":""}"#, None);
let data = ctrl.post_data(req).await.unwrap();
let name = data["name"].as_str().unwrap();
if name.is_empty() {
let resp = ctrl.render_error("用户名不能为空", json!({"field": "name"}), 0);
let body = collect_body(resp).await;
assert_eq!(
body,
r#"{"code":0,"msg":"用户名不能为空","data":{"field":"name"}}"#
);
} else {
panic!("should be empty");
}
}
#[tokio::test]
async fn test_php_consistency_not_login_flow() {
struct PassportController;
impl SzController for PassportController {}
let ctrl = PassportController;
let resp = ctrl.render_error("not_login", json!({}), -1);
let body = collect_body(resp).await;
assert_eq!(body, r#"{"code":-1,"msg":"not_login","data":{}}"#);
}
struct MockBaseController;
impl SzController for MockBaseController {}
impl BaseController for MockBaseController {}
#[test]
fn test_base_controller_default_batch_validate() {
let ctrl = MockBaseController;
assert!(!ctrl.batch_validate());
}
#[test]
fn test_base_controller_default_middlewares_empty() {
let ctrl = MockBaseController;
assert!(ctrl.middlewares().is_empty());
}
#[test]
fn test_base_controller_default_initialize_no_panic() {
let ctrl = MockBaseController;
ctrl.initialize(); }
#[test]
fn test_base_controller_default_validate_returns_ok() {
let ctrl = MockBaseController;
let data = json!({"name": "alice"});
let rules = [("name", "require")];
let messages: [(&str, &str); 0] = [];
let result = ctrl.validate(&data, &rules, &messages);
assert!(result.is_ok());
}
#[test]
fn test_validate_require_pass_with_value() {
let ctrl = MockBaseController;
let data = json!({"name": "alice", "age": 30});
let rules = [("name", "require"), ("age", "require|integer")];
let messages: [(&str, &str); 0] = [];
assert!(ctrl.validate(&data, &rules, &messages).is_ok());
}
#[test]
fn test_validate_require_fail_when_missing() {
let ctrl = MockBaseController;
let data = json!({"name": "alice"});
let rules = [("name", "require"), ("age", "require|integer")];
let messages: [(&str, &str); 0] = [];
let result = ctrl.validate(&data, &rules, &messages);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.contains("age"), "error: {err}");
}
#[test]
fn test_validate_integer_fail_on_string() {
let ctrl = MockBaseController;
let data = json!({"age": "not-a-number"});
let rules = [("age", "require|integer")];
let messages: [(&str, &str); 0] = [];
let result = ctrl.validate(&data, &rules, &messages);
assert!(result.is_err());
}
#[test]
fn test_validate_custom_message_applied() {
let ctrl = MockBaseController;
let data = json!({}); let rules = [("name", "require")];
let messages = [("name.require", "名称必填")];
let result = ctrl.validate(&data, &rules, &messages);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "名称必填");
}
#[test]
fn test_validate_batch_mode_returns_multiple_errors() {
struct BatchController;
impl SzController for BatchController {}
impl BaseController for BatchController {
fn batch_validate(&self) -> bool {
true
}
}
let ctrl = BatchController;
let data = json!({}); let rules = [("name", "require"), ("age", "require")];
let messages = [("name.require", "名称必填"), ("age.require", "年龄必填")];
let result = ctrl.validate(&data, &rules, &messages);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.contains("名称必填"), "err: {err}");
assert!(err.contains("年龄必填"), "err: {err}");
assert!(err.contains("; "), "err: {err}");
}
#[test]
fn test_validate_single_mode_returns_first_error_only() {
let ctrl = MockBaseController;
let data = json!({}); let rules = [("name", "require"), ("age", "require")];
let messages = [("name.require", "名称必填"), ("age.require", "年龄必填")];
let result = ctrl.validate(&data, &rules, &messages);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.contains("名称必填"), "err: {err}");
assert!(!err.contains("年龄必填"), "err: {err}");
}
#[test]
fn test_validate_in_rule_pass() {
let ctrl = MockBaseController;
let data = json!({"status": "active"});
let rules = [("status", "require|in:active,inactive")];
let messages: [(&str, &str); 0] = [];
assert!(ctrl.validate(&data, &rules, &messages).is_ok());
}
#[test]
fn test_validate_in_rule_fail() {
let ctrl = MockBaseController;
let data = json!({"status": "deleted"});
let rules = [("status", "require|in:active,inactive")];
let messages: [(&str, &str); 0] = [];
assert!(ctrl.validate(&data, &rules, &messages).is_err());
}
#[test]
fn test_validate_empty_rules_always_pass() {
let ctrl = MockBaseController;
let data = json!({"anything": "value"});
let rules: [(&str, &str); 0] = [];
let messages: [(&str, &str); 0] = [];
assert!(ctrl.validate(&data, &rules, &messages).is_ok());
}
#[test]
fn test_base_controller_inherits_sz_controller_methods() {
let ctrl = MockBaseController;
let value = ctrl.render_json(1, "ok", json!({}));
assert_eq!(value["code"], 1);
assert_eq!(value["msg"], "ok");
let resp = ctrl.render_success("ok", json!({"id": 1}));
assert_eq!(resp.status(), StatusCode::OK);
}
struct CustomController;
impl SzController for CustomController {}
impl BaseController for CustomController {
fn batch_validate(&self) -> bool {
true
}
fn middlewares(&self) -> Vec<String> {
vec!["auth".to_string(), "cors".to_string()]
}
fn initialize(&self) {
}
}
#[test]
fn test_base_controller_override_batch_validate() {
let ctrl = CustomController;
assert!(ctrl.batch_validate());
}
#[test]
fn test_base_controller_override_middlewares() {
let ctrl = CustomController;
let mws = ctrl.middlewares();
assert_eq!(mws, vec!["auth", "cors"]);
}
#[test]
fn test_base_controller_override_initialize() {
let ctrl = CustomController;
ctrl.initialize(); }
struct StatefulController {
initialized: parking_lot::Mutex<bool>,
custom_batch: bool,
}
impl StatefulController {
fn new() -> Self {
Self {
initialized: parking_lot::Mutex::new(false),
custom_batch: false,
}
}
}
impl SzController for StatefulController {}
impl BaseController for StatefulController {
fn batch_validate(&self) -> bool {
self.custom_batch
}
fn initialize(&self) {
*self.initialized.lock() = true;
}
}
#[test]
fn test_base_controller_stateful_initialize() {
let ctrl = StatefulController::new();
assert!(!*ctrl.initialized.lock()); ctrl.initialize(); assert!(*ctrl.initialized.lock()); }
#[tokio::test]
async fn test_base_controller_php_full_flow() {
struct UserController;
impl SzController for UserController {}
impl BaseController for UserController {}
let ctrl = UserController;
ctrl.initialize();
let req = make_json_request(r#"{"name":"alice","age":30}"#, None);
let data = ctrl.post_data(req).await.unwrap();
let rules = [("name", "require"), ("age", "require|integer|gt:0")];
let messages: [(&str, &str); 0] = [];
ctrl.validate(&data, &rules, &messages).unwrap();
let resp =
ctrl.render_success("success", json!({"name": data["name"], "age": data["age"]}));
let body = collect_body(resp).await;
assert_eq!(
body,
r#"{"code":1,"msg":"success","data":{"name":"alice","age":30}}"#
);
}
#[test]
fn test_base_controller_multiple_instances_independent() {
struct ControllerA;
struct ControllerB;
impl SzController for ControllerA {}
impl BaseController for ControllerA {
fn middlewares(&self) -> Vec<String> {
vec!["auth".to_string()]
}
}
impl SzController for ControllerB {}
impl BaseController for ControllerB {
fn middlewares(&self) -> Vec<String> {
vec!["cors".to_string(), "log".to_string()]
}
}
let a = ControllerA;
let b = ControllerB;
assert_eq!(a.middlewares(), vec!["auth"]);
assert_eq!(b.middlewares(), vec!["cors", "log"]);
assert!(!a.batch_validate());
assert!(!b.batch_validate());
}
#[test]
fn test_base_controller_inheritance_chain() {
struct BusinessController;
impl SzController for BusinessController {}
impl BaseController for BusinessController {}
let ctrl = BusinessController;
let value = ctrl.render_json(0, "error", json!({}));
assert!(value.is_object());
assert!(!ctrl.batch_validate());
assert!(ctrl.middlewares().is_empty());
ctrl.initialize();
}
struct MockAddonsController;
impl SzController for MockAddonsController {}
impl BaseController for MockAddonsController {}
impl AddonsBaseController for MockAddonsController {}
#[test]
fn test_addons_default_allow_all_action() {
let ctrl = MockAddonsController;
let allow = ctrl.allow_all_action();
assert!(allow.contains(&"/passport/login"));
assert!(allow.contains(&"/task/task/userClerk"));
assert_eq!(allow.len(), 2);
}
#[test]
fn test_addons_parse_route_info_two_segments() {
let ctrl = MockAddonsController;
let info = ctrl.parse_route_info("/passport/login");
assert_eq!(info.controller, "passport");
assert_eq!(info.action, "login");
assert_eq!(info.group, "passport");
assert_eq!(info.route_uri, "/passport/login");
}
#[test]
fn test_addons_parse_route_info_three_segments() {
let ctrl = MockAddonsController;
let info = ctrl.parse_route_info("/task/task/userClerk");
assert_eq!(info.controller, "task/task");
assert_eq!(info.action, "userClerk");
assert_eq!(info.group, "task/task"); assert_eq!(info.route_uri, "/task/task/userClerk");
}
#[test]
fn test_addons_parse_route_info_single_segment() {
let ctrl = MockAddonsController;
let info = ctrl.parse_route_info("/passport");
assert_eq!(info.controller, "passport");
assert_eq!(info.action, "");
assert_eq!(info.group, "passport");
assert_eq!(info.route_uri, "/passport/");
}
#[test]
fn test_addons_parse_route_info_root() {
let ctrl = MockAddonsController;
let info = ctrl.parse_route_info("/");
assert_eq!(info.controller, "");
assert_eq!(info.action, "");
assert_eq!(info.group, "");
assert_eq!(info.route_uri, "/");
}
#[test]
fn test_addons_parse_route_info_empty_uri() {
let ctrl = MockAddonsController;
let info = ctrl.parse_route_info("");
assert_eq!(info.controller, "");
assert_eq!(info.route_uri, "/");
}
#[test]
fn test_addons_parse_route_info_with_query_string() {
let ctrl = MockAddonsController;
let info = ctrl.parse_route_info("/user/info?foo=bar&baz=1");
assert_eq!(info.controller, "user");
assert_eq!(info.action, "info");
assert_eq!(info.route_uri, "/user/info");
}
#[test]
fn test_addons_parse_route_info_trailing_slash() {
let ctrl = MockAddonsController;
let info = ctrl.parse_route_info("/user/info/");
assert_eq!(info.controller, "user/info");
assert_eq!(info.action, "");
assert_eq!(info.route_uri, "/user/info/");
}
#[test]
fn test_addons_check_login_whitelist_pass() {
let ctrl = MockAddonsController;
let result = ctrl.check_login("/passport/login", false);
assert!(result.is_ok());
}
#[test]
fn test_addons_check_login_whitelist_pass_2() {
let ctrl = MockAddonsController;
let result = ctrl.check_login("/task/task/userClerk", false);
assert!(result.is_ok());
}
#[test]
fn test_addons_check_login_logged_in_pass() {
let ctrl = MockAddonsController;
let result = ctrl.check_login("/user/info", true);
assert!(result.is_ok());
}
#[test]
fn test_addons_check_login_not_logged_in_fail() {
let ctrl = MockAddonsController;
let result = ctrl.check_login("/user/info", false);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "not_login");
}
#[test]
fn test_addons_get_token_default_returns_none() {
let ctrl = MockAddonsController;
let result = ctrl.get_token(Some("Bearer xxx.yyy.zzz"));
assert!(result.is_ok());
assert_eq!(result.unwrap(), None);
}
#[test]
fn test_addons_get_token_no_authorization() {
let ctrl = MockAddonsController;
let result = ctrl.get_token(None);
assert!(result.is_ok());
assert_eq!(result.unwrap(), None);
}
#[test]
fn test_addons_get_token_empty_authorization() {
let ctrl = MockAddonsController;
let result = ctrl.get_token(Some(""));
assert!(result.is_ok());
assert_eq!(result.unwrap(), None);
}
#[test]
fn test_addons_get_token_invalid_format_returns_none() {
let ctrl = MockAddonsController;
let result = ctrl.get_token(Some("Bearer not.a.valid.jwt.token"));
assert!(result.is_ok());
assert_eq!(result.unwrap(), None);
}
#[test]
fn test_strip_bearer_prefix_uppercase() {
assert_eq!(strip_bearer_prefix("Bearer abc.def.ghi"), "abc.def.ghi");
}
#[test]
fn test_strip_bearer_prefix_lowercase() {
assert_eq!(strip_bearer_prefix("bearer abc.def.ghi"), "abc.def.ghi");
}
#[test]
fn test_strip_bearer_prefix_mixed_case() {
assert_eq!(strip_bearer_prefix("BEARER abc.def.ghi"), "abc.def.ghi");
}
#[test]
fn test_strip_bearer_prefix_no_prefix() {
assert_eq!(strip_bearer_prefix("abc.def.ghi"), "abc.def.ghi");
}
#[test]
fn test_strip_bearer_prefix_empty() {
assert_eq!(strip_bearer_prefix(""), "");
}
#[test]
fn test_strip_bearer_prefix_with_extra_spaces() {
assert_eq!(
strip_bearer_prefix(" Bearer abc.def.ghi "),
"abc.def.ghi"
);
}
#[test]
fn test_get_token_valid_jwt_returns_user_info() {
let config = JwtConfig {
secret: "test-secret".to_string(),
issuer: String::new(), };
let encoder = sz_orm_auth::jwt::JwtEncoder::new(&config.secret);
let exp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64
+ 3600; let claims = sz_orm_auth::jwt::JwtClaims::new("user123", exp).with_user_id(12345);
let token = encoder.encode(&claims).unwrap();
let result = verify_token_with_config(Some(&format!("Bearer {token}")), &config);
assert!(result.is_ok());
let user = result.unwrap();
assert!(user.is_some());
let user = user.unwrap();
assert_eq!(user.user_id, 12345);
assert!(user.is_login);
}
#[test]
fn test_get_token_wrong_secret_returns_none() {
let config = JwtConfig {
secret: "correct-secret".to_string(),
issuer: String::new(),
};
let encoder = sz_orm_auth::jwt::JwtEncoder::new("wrong-secret");
let exp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64
+ 3600;
let claims = sz_orm_auth::jwt::JwtClaims::new("user123", exp).with_user_id(12345);
let token = encoder.encode(&claims).unwrap();
let result = verify_token_with_config(Some(&format!("Bearer {token}")), &config);
assert!(result.is_ok());
assert_eq!(result.unwrap(), None); }
#[test]
fn test_get_token_expired_returns_none() {
let config = JwtConfig {
secret: "test-secret".to_string(),
issuer: String::new(),
};
let encoder = sz_orm_auth::jwt::JwtEncoder::new(&config.secret);
let exp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64
- 3600;
let claims = sz_orm_auth::jwt::JwtClaims::new("user123", exp).with_user_id(12345);
let token = encoder.encode(&claims).unwrap();
let result = verify_token_with_config(Some(&format!("Bearer {token}")), &config);
assert!(result.is_ok());
assert_eq!(result.unwrap(), None); }
#[test]
fn test_get_token_no_user_id_claim_returns_none() {
let config = JwtConfig {
secret: "test-secret".to_string(),
issuer: String::new(),
};
let encoder = sz_orm_auth::jwt::JwtEncoder::new(&config.secret);
let exp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64
+ 3600;
let claims = sz_orm_auth::jwt::JwtClaims::new("user123", exp);
let token = encoder.encode(&claims).unwrap();
let result = verify_token_with_config(Some(&format!("Bearer {token}")), &config);
assert!(result.is_ok());
assert_eq!(result.unwrap(), None); }
#[test]
fn test_get_token_iss_mismatch_returns_none() {
let config = JwtConfig {
secret: "test-secret".to_string(),
issuer: "https://expected-issuer.com".to_string(),
};
let encoder = sz_orm_auth::jwt::JwtEncoder::new(&config.secret);
let exp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64
+ 3600;
let claims = sz_orm_auth::jwt::JwtClaims::new("user123", exp)
.with_issuer("https://wrong-issuer.com")
.with_user_id(12345);
let token = encoder.encode(&claims).unwrap();
let result = verify_token_with_config(Some(&format!("Bearer {token}")), &config);
assert!(result.is_ok());
assert_eq!(result.unwrap(), None); }
#[test]
fn test_get_token_iss_match_returns_user_info() {
let config = JwtConfig {
secret: "test-secret".to_string(),
issuer: "https://mall.ljclz.shop".to_string(),
};
let encoder = sz_orm_auth::jwt::JwtEncoder::new(&config.secret);
let exp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64
+ 3600;
let claims = sz_orm_auth::jwt::JwtClaims::new("user123", exp)
.with_issuer(&config.issuer)
.with_user_id(67890);
let token = encoder.encode(&claims).unwrap();
let result = verify_token_with_config(Some(&format!("Bearer {token}")), &config);
assert!(result.is_ok());
let user = result.unwrap().unwrap();
assert_eq!(user.user_id, 67890);
assert!(user.is_login);
}
#[test]
fn test_get_token_empty_secret_returns_none() {
let config = JwtConfig::default();
let result = verify_token_with_config(Some("Bearer any.token.here"), &config);
assert!(result.is_ok());
assert_eq!(result.unwrap(), None); }
#[test]
fn test_get_token_without_bearer_prefix() {
let config = JwtConfig {
secret: "test-secret".to_string(),
issuer: String::new(),
};
let encoder = sz_orm_auth::jwt::JwtEncoder::new(&config.secret);
let exp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64
+ 3600;
let claims = sz_orm_auth::jwt::JwtClaims::new("user123", exp).with_user_id(99999);
let token = encoder.encode(&claims).unwrap();
let result = verify_token_with_config(Some(&token), &config);
assert!(result.is_ok());
let user = result.unwrap().unwrap();
assert_eq!(user.user_id, 99999);
}
struct CustomAddonsController;
impl SzController for CustomAddonsController {}
impl BaseController for CustomAddonsController {}
impl AddonsBaseController for CustomAddonsController {
fn allow_all_action(&self) -> Vec<&'static str> {
vec!["/custom/public", "/custom/health"]
}
}
#[test]
fn test_addons_override_allow_all_action() {
let ctrl = CustomAddonsController;
let allow = ctrl.allow_all_action();
assert_eq!(allow, vec!["/custom/public", "/custom/health"]);
assert!(!allow.contains(&"/passport/login"));
}
#[test]
fn test_addons_override_check_login_uses_new_whitelist() {
let ctrl = CustomAddonsController;
assert!(ctrl.check_login("/custom/public", false).is_ok());
assert!(ctrl.check_login("/passport/login", false).is_err());
}
#[tokio::test]
async fn test_addons_php_full_flow_whitelist() {
struct PassportController;
impl SzController for PassportController {}
impl BaseController for PassportController {}
impl AddonsBaseController for PassportController {}
let ctrl = PassportController;
let info = ctrl.parse_route_info("/passport/login");
assert_eq!(info.route_uri, "/passport/login");
let user = ctrl.get_token(None).unwrap();
let is_login = user.as_ref().is_some_and(|u| u.is_login);
ctrl.check_login(&info.route_uri, is_login).unwrap();
let resp = ctrl.render_success("登录成功", json!({"token": "fake.jwt.token"}));
let body = collect_body(resp).await;
assert_eq!(
body,
r#"{"code":1,"msg":"登录成功","data":{"token":"fake.jwt.token"}}"#
);
}
#[tokio::test]
async fn test_addons_php_full_flow_not_login() {
struct UserController;
impl SzController for UserController {}
impl BaseController for UserController {}
impl AddonsBaseController for UserController {}
let ctrl = UserController;
let info = ctrl.parse_route_info("/user/info");
let user = ctrl.get_token(None).unwrap();
let is_login = user.as_ref().is_some_and(|u| u.is_login);
let result = ctrl.check_login(&info.route_uri, is_login);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "not_login");
let resp = ctrl.render_error("not_login", json!({}), -1);
let body = collect_body(resp).await;
assert_eq!(body, r#"{"code":-1,"msg":"not_login","data":{}}"#);
}
#[tokio::test]
async fn test_addons_php_full_flow_logged_in() {
struct OrderController;
impl SzController for OrderController {}
impl BaseController for OrderController {}
impl AddonsBaseController for OrderController {
fn get_token(&self, _authorization: Option<&str>) -> Result<Option<UserInfo>, String> {
Ok(Some(UserInfo {
user_id: 12345,
is_login: true,
}))
}
}
let ctrl = OrderController;
let info = ctrl.parse_route_info("/order/list");
let user = ctrl.get_token(None).unwrap();
let is_login = user.as_ref().is_some_and(|u| u.is_login);
ctrl.check_login(&info.route_uri, is_login).unwrap();
let resp = ctrl.render_success(
"success",
json!({"list": [{"id": 1}, {"id": 2}], "total": 2}),
);
let body = collect_body(resp).await;
assert_eq!(
body,
r#"{"code":1,"msg":"success","data":{"list":[{"id":1},{"id":2}],"total":2}}"#
);
}
#[test]
fn test_route_info_debug_clone_eq() {
let info1 = RouteInfo {
controller: "user".to_string(),
action: "info".to_string(),
route_uri: "/user/info".to_string(),
group: "user".to_string(),
};
let info2 = info1.clone();
assert_eq!(info1, info2);
let debug_str = format!("{info1:?}");
assert!(debug_str.contains("RouteInfo"));
assert!(debug_str.contains("user"));
}
#[test]
fn test_user_info_debug_clone_eq() {
let user1 = UserInfo {
user_id: 12345,
is_login: true,
};
let user2 = user1.clone();
assert_eq!(user1, user2);
let debug_str = format!("{user1:?}");
assert!(debug_str.contains("UserInfo"));
assert!(debug_str.contains("12345"));
}
#[test]
fn test_addons_inheritance_chain_all_methods() {
struct BusinessController;
impl SzController for BusinessController {}
impl BaseController for BusinessController {}
impl AddonsBaseController for BusinessController {}
let ctrl = BusinessController;
let value = ctrl.render_json(1, "ok", json!({}));
assert_eq!(value["code"], 1);
assert!(!ctrl.batch_validate());
assert!(ctrl.middlewares().is_empty());
ctrl.initialize();
let info = ctrl.parse_route_info("/test/action");
assert_eq!(info.route_uri, "/test/action");
assert!(ctrl.check_login("/passport/login", false).is_ok());
assert!(ctrl.get_token(None).unwrap().is_none());
}
}