use sa_token_core::token::TokenValue;
#[cfg(feature = "with-summer-redis")]
use std::time::{SystemTime, UNIX_EPOCH};
use summer::app::AppBuilder;
use summer::async_trait;
use summer::plugin::{ComponentRegistry, MutableComponentRegistry, Plugin};
use summer_sa_token::sa_token_plugin_axum::{
OptionalSaTokenExtractor, SaTokenLayer, SaTokenState, StpUtil,
};
use summer_web::axum::{
body::Body,
http::{Request, StatusCode},
response::IntoResponse,
routing::get,
Router,
};
use tower::ServiceExt;
#[cfg(feature = "with-summer-redis")]
use summer_sa_token::storage::SummerRedisStorage;
#[cfg(feature = "with-summer-redis")]
use summer_redis::redis::AsyncCommands;
struct TestSaTokenPlugin;
#[async_trait]
impl Plugin for TestSaTokenPlugin {
async fn build(&self, app: &mut AppBuilder) {
let state = SaTokenState::builder()
.storage(std::sync::Arc::new(
summer_sa_token::sa_token_plugin_axum::MemoryStorage::new(),
))
.token_name("Authorization".to_string())
.timeout(3600)
.build();
app.add_component(state);
}
fn name(&self) -> &str {
"TestSaTokenPlugin"
}
}
#[tokio::test]
async fn test_sa_token_integration() {
let mut app = AppBuilder::default();
TestSaTokenPlugin.build(&mut app).await;
let state = app
.get_component::<SaTokenState>()
.expect("SaTokenState should be registered");
{
let _layer = SaTokenLayer::new(state.clone());
}
{
let token = state.manager.login("test_user").await.unwrap();
assert!(!token.as_str().is_empty());
let is_valid = state.manager.is_valid(&token).await;
assert!(is_valid);
let token_info = state.manager.get_token_info(&token).await;
assert!(token_info.is_ok());
assert_eq!(token_info.unwrap().login_id, "test_user");
}
{
let token = state.manager.login("logout_user").await.unwrap();
assert!(state.manager.is_valid(&token).await);
state
.manager
.logout_by_login_id("logout_user")
.await
.unwrap();
assert!(!state.manager.is_valid(&token).await);
}
{
let token1 = state.manager.login("multi_user").await.unwrap();
let token2 = state.manager.login("multi_user").await.unwrap();
assert!(state.manager.is_valid(&token1).await);
assert!(state.manager.is_valid(&token2).await);
}
{
let token = state.manager.login("info_user").await.unwrap();
let info = state.manager.get_token_info(&token).await.unwrap();
assert_eq!(info.login_id, "info_user");
}
{
let fake_token = TokenValue::new("fake-token-12345");
assert!(!state.manager.is_valid(&fake_token).await);
}
{
let _token = state.manager.login("role_user").await.unwrap();
StpUtil::set_roles("role_user", vec!["admin".to_string(), "user".to_string()])
.await
.unwrap();
StpUtil::set_permissions(
"role_user",
vec!["user:read".to_string(), "user:write".to_string()],
)
.await
.unwrap();
assert!(StpUtil::has_role("role_user", "admin").await);
assert!(StpUtil::has_role("role_user", "user").await);
assert!(!StpUtil::has_role("role_user", "superadmin").await);
assert!(StpUtil::has_permission("role_user", "user:read").await);
assert!(StpUtil::has_permission("role_user", "user:write").await);
assert!(!StpUtil::has_permission("role_user", "user:delete").await);
let roles = StpUtil::get_roles("role_user").await;
assert_eq!(roles.len(), 2);
assert!(roles.contains(&"admin".to_string()));
let permissions = StpUtil::get_permissions("role_user").await;
assert_eq!(permissions.len(), 2);
assert!(permissions.contains(&"user:read".to_string()));
}
{
let token = state.manager.login("middleware_user").await.unwrap();
async fn handler(optional_token: OptionalSaTokenExtractor) -> impl IntoResponse {
match optional_token.0 {
Some(token) => format!("Token: {}", token.as_str()),
None => "No token".to_string(),
}
}
let app = Router::new()
.route("/test", get(handler))
.layer(SaTokenLayer::new(state.clone()));
let response = app
.oneshot(
Request::builder()
.uri("/test")
.header("Authorization", token.as_str())
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
{
async fn handler(optional_token: OptionalSaTokenExtractor) -> impl IntoResponse {
match optional_token.0 {
Some(_) => "Has token",
None => "No token",
}
}
let app = Router::new()
.route("/test", get(handler))
.layer(SaTokenLayer::new(state.clone()));
let response = app
.oneshot(Request::builder().uri("/test").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
}
#[cfg(feature = "with-summer-redis")]
async fn create_redis_connection() -> Option<summer_redis::Redis> {
let url = std::env::var("REDIS_URL").ok()?;
let client = summer_redis::redis::Client::open(url).expect("redis client should open");
Some(
client
.get_connection_manager()
.await
.expect("redis connection manager should connect"),
)
}
#[cfg(feature = "with-summer-redis")]
fn unique_storage_prefix(tag: &str) -> String {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("time should be after epoch")
.as_nanos();
format!("demo-{tag}-{nanos}")
}
#[cfg(feature = "with-summer-redis")]
#[tokio::test]
async fn test_redis_storage_rewrites_prefix_for_physical_keys() {
let Some(mut redis) = create_redis_connection().await else {
eprintln!("skipping: REDIS_URL is not set");
return;
};
let prefix = unique_storage_prefix("rewrite");
let storage = SummerRedisStorage::new(redis.clone(), Some(prefix.clone()), true);
storage.clear().await.expect("clear should succeed");
storage
.set("sa:login:token:user1", "token-123", None)
.await
.expect("set should succeed");
let rewritten_key = format!("{prefix}:login:token:user1");
let raw: Option<String> = redis
.get(&rewritten_key)
.await
.expect("prefixed key should be readable");
assert_eq!(raw.as_deref(), Some("token-123"));
let legacy_key = format!("{prefix}:sa:login:token:user1");
let old_raw: Option<String> = redis
.get(&legacy_key)
.await
.expect("old prefixed key should be readable");
assert!(old_raw.is_none());
storage.clear().await.expect("clear should succeed");
}
#[cfg(feature = "with-summer-redis")]
#[tokio::test]
async fn test_redis_storage_keys_and_clear_work_with_rewritten_prefix() {
let Some(mut redis) = create_redis_connection().await else {
eprintln!("skipping: REDIS_URL is not set");
return;
};
let prefix = unique_storage_prefix("keys");
let storage = SummerRedisStorage::new(redis.clone(), Some(prefix.clone()), true);
storage.clear().await.expect("clear should succeed");
redis
.set::<_, _, ()>(format!("{prefix}:token:one"), "a")
.await
.expect("set should succeed");
redis
.set::<_, _, ()>(format!("{prefix}:session:user1"), "b")
.await
.expect("set should succeed");
redis
.set::<_, _, ()>(format!("{prefix}:refresh:token-1"), "c")
.await
.expect("set should succeed");
let mut keys = storage.keys("sa:*").await.expect("keys should succeed");
keys.sort();
assert_eq!(
keys,
vec![
"sa:refresh:token-1".to_string(),
"sa:session:user1".to_string(),
"sa:token:one".to_string()
]
);
storage.clear().await.expect("clear should succeed");
let remaining: Vec<String> = redis
.keys(format!("{prefix}:*"))
.await
.expect("keys should succeed");
assert!(remaining.is_empty());
}
mod test_config_conversion {
use summer_sa_token::{CoreConfig, SaTokenConfig};
#[test]
fn test_config_into_core_config() {
let config = SaTokenConfig {
token_name: "TestToken".to_string(),
timeout: 7200,
auto_renew: true,
..Default::default()
};
let core_config: CoreConfig = config.into();
assert_eq!(core_config.token_name, "TestToken");
assert_eq!(core_config.timeout, 7200);
assert!(core_config.auto_renew);
}
}
mod test_path_auth_builder {
use summer_sa_token::PathAuthBuilder;
#[test]
fn test_path_auth_builder_creation() {
let builder = PathAuthBuilder::new();
assert!(!builder.is_configured());
}
#[test]
fn test_path_auth_builder_include() {
let builder = PathAuthBuilder::new()
.include("/api/**")
.include("/admin/**");
assert!(builder.is_configured());
}
#[test]
fn test_path_auth_builder_exclude() {
let builder = PathAuthBuilder::new()
.include("/api/**")
.exclude("/api/public/**")
.exclude("/login");
assert!(builder.is_configured());
}
#[test]
fn test_path_auth_builder_include_all() {
let builder = PathAuthBuilder::new().include_all(["/api/**", "/admin/**", "/user/**"]);
assert!(builder.is_configured());
}
#[test]
fn test_path_auth_builder_exclude_all() {
let builder = PathAuthBuilder::new()
.include("/api/**")
.exclude_all(["/api/public/**", "/api/health"]);
assert!(builder.is_configured());
}
#[test]
fn test_path_auth_builder_aliases() {
let builder = PathAuthBuilder::new()
.authenticated("/api/**")
.permit_all("/api/public/**");
assert!(builder.is_configured());
}
#[test]
fn test_path_auth_builder_merge() {
let builder1 = PathAuthBuilder::new().include("/api/**");
let builder2 = PathAuthBuilder::new().include("/admin/**");
let merged = builder1.merge(builder2);
assert!(merged.is_configured());
}
#[test]
fn test_path_auth_builder_build() {
let builder = PathAuthBuilder::new()
.include("/api/**")
.exclude("/api/public/**");
let _config = builder.build();
}
}