#![cfg(native)]
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestUser {
pub id: Uuid,
pub username: String,
pub email: String,
pub permissions: Vec<String>,
pub roles: Vec<String>,
pub is_authenticated: bool,
pub attributes: HashMap<String, Value>,
}
impl Default for TestUser {
fn default() -> Self {
Self {
id: Uuid::now_v7(),
username: String::new(),
email: String::new(),
permissions: Vec::new(),
roles: Vec::new(),
is_authenticated: false,
attributes: HashMap::new(),
}
}
}
impl TestUser {
pub fn anonymous() -> Self {
Self::default()
}
pub fn authenticated(username: impl Into<String>) -> Self {
let username = username.into();
Self {
id: Uuid::now_v7(),
email: format!("{}@test.example.com", username),
username,
is_authenticated: true,
..Default::default()
}
}
pub fn admin() -> Self {
Self {
id: Uuid::now_v7(),
username: "admin".to_string(),
email: "admin@test.example.com".to_string(),
permissions: vec![
"admin".to_string(),
"*".to_string(), ],
roles: vec!["admin".to_string(), "superuser".to_string()],
is_authenticated: true,
attributes: HashMap::new(),
}
}
pub fn with_id(mut self, id: Uuid) -> Self {
self.id = id;
self
}
pub fn with_email(mut self, email: impl Into<String>) -> Self {
self.email = email.into();
self
}
pub fn with_permission(mut self, permission: impl Into<String>) -> Self {
self.permissions.push(permission.into());
self
}
pub fn with_permissions<S: Into<String>>(
mut self,
permissions: impl IntoIterator<Item = S>,
) -> Self {
for perm in permissions {
self.permissions.push(perm.into());
}
self
}
pub fn with_role(mut self, role: impl Into<String>) -> Self {
self.roles.push(role.into());
self
}
pub fn with_roles<S: Into<String>>(mut self, roles: impl IntoIterator<Item = S>) -> Self {
for role in roles {
self.roles.push(role.into());
}
self
}
pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
self.attributes.insert(key.into(), value.into());
self
}
pub fn has_permission(&self, permission: &str) -> bool {
self.permissions.iter().any(|p| p == permission || p == "*")
}
pub fn has_role(&self, role: &str) -> bool {
self.roles.iter().any(|r| r == role)
}
pub fn has_any_permission(&self, permissions: &[&str]) -> bool {
permissions.iter().any(|p| self.has_permission(p))
}
pub fn has_all_permissions(&self, permissions: &[&str]) -> bool {
permissions.iter().all(|p| self.has_permission(p))
}
pub fn get_attribute(&self, key: &str) -> Option<&Value> {
self.attributes.get(key)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct MockSession {
pub id: String,
pub user: Option<TestUser>,
pub data: HashMap<String, Value>,
pub csrf_token: String,
pub created_at: i64,
pub expires_at: Option<i64>,
pub invalidated: bool,
}
impl MockSession {
pub fn anonymous() -> Self {
Self {
id: Uuid::now_v7().to_string(),
user: None,
data: HashMap::new(),
csrf_token: generate_csrf_token(),
created_at: chrono::Utc::now().timestamp(),
expires_at: None,
invalidated: false,
}
}
pub fn authenticated(user: TestUser) -> Self {
Self {
id: Uuid::now_v7().to_string(),
user: Some(user),
data: HashMap::new(),
csrf_token: generate_csrf_token(),
created_at: chrono::Utc::now().timestamp(),
expires_at: None,
invalidated: false,
}
}
pub fn from_identity(identity: &crate::auth::SessionIdentity) -> Self {
let stub_user = TestUser {
id: uuid::Uuid::parse_str(&identity.user_id).unwrap_or(uuid::Uuid::nil()),
username: identity.user_id.clone(),
email: String::new(),
permissions: Vec::new(),
roles: Vec::new(),
is_authenticated: true,
attributes: HashMap::new(),
};
let mut session = Self::authenticated(stub_user);
session
.data
.insert("user_id".into(), serde_json::json!(identity.user_id));
session
.data
.insert("is_staff".into(), serde_json::json!(identity.is_staff));
session.data.insert(
"is_superuser".into(),
serde_json::json!(identity.is_superuser),
);
session
}
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.id = id.into();
self
}
pub fn with_data(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
self.data.insert(key.into(), value.into());
self
}
pub fn with_csrf_token(mut self, token: impl Into<String>) -> Self {
self.csrf_token = token.into();
self
}
pub fn with_expiration(mut self, expires_at: i64) -> Self {
self.expires_at = Some(expires_at);
self
}
pub fn expires_in(mut self, seconds: i64) -> Self {
self.expires_at = Some(chrono::Utc::now().timestamp() + seconds);
self
}
pub fn is_authenticated(&self) -> bool {
self.user.is_some() && !self.invalidated
}
pub fn is_expired(&self) -> bool {
if let Some(expires_at) = self.expires_at {
chrono::Utc::now().timestamp() > expires_at
} else {
false
}
}
pub fn is_valid(&self) -> bool {
!self.invalidated && !self.is_expired()
}
pub fn get<T: for<'de> Deserialize<'de>>(&self, key: &str) -> Option<T> {
self.data
.get(key)
.and_then(|v| serde_json::from_value(v.clone()).ok())
}
pub fn get_raw(&self, key: &str) -> Option<&Value> {
self.data.get(key)
}
pub fn set(&mut self, key: impl Into<String>, value: impl Into<Value>) {
self.data.insert(key.into(), value.into());
}
pub fn remove(&mut self, key: &str) -> Option<Value> {
self.data.remove(key)
}
pub fn clear_data(&mut self) {
self.data.clear();
}
pub fn invalidate(&mut self) {
self.invalidated = true;
}
pub fn user_id(&self) -> Option<Uuid> {
self.user.as_ref().map(|u| u.id)
}
pub fn regenerate_id(&mut self) {
self.id = Uuid::now_v7().to_string();
}
pub fn regenerate_csrf(&mut self) {
self.csrf_token = generate_csrf_token();
}
pub fn verify_csrf(&self, token: &str) -> bool {
!token.is_empty() && self.csrf_token == token
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestTokenClaims {
pub sub: String,
pub iat: i64,
pub exp: i64,
pub iss: Option<String>,
pub aud: Option<String>,
#[serde(flatten)]
pub custom: HashMap<String, Value>,
}
impl TestTokenClaims {
pub fn for_user(user: &TestUser) -> Self {
let now = chrono::Utc::now().timestamp();
Self {
sub: user.id.to_string(),
iat: now,
exp: now + 3600, iss: None,
aud: None,
custom: HashMap::new(),
}
}
pub fn expires_in(mut self, seconds: i64) -> Self {
self.exp = chrono::Utc::now().timestamp() + seconds;
self
}
pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
self.iss = Some(issuer.into());
self
}
pub fn with_audience(mut self, audience: impl Into<String>) -> Self {
self.aud = Some(audience.into());
self
}
pub fn with_claim(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
self.custom.insert(key.into(), value.into());
self
}
pub fn is_expired(&self) -> bool {
chrono::Utc::now().timestamp() > self.exp
}
}
fn generate_csrf_token() -> String {
Uuid::now_v7().to_string().replace('-', "")
}
pub mod assert_permissions {
use super::*;
pub fn has_permission(user: &TestUser, permission: &str) {
assert!(
user.has_permission(permission),
"Expected user '{}' to have permission '{}', but they don't.\nActual permissions: {:?}",
user.username,
permission,
user.permissions
);
}
pub fn lacks_permission(user: &TestUser, permission: &str) {
assert!(
!user.has_permission(permission),
"Expected user '{}' to NOT have permission '{}', but they do.\nActual permissions: {:?}",
user.username,
permission,
user.permissions
);
}
pub fn has_role(user: &TestUser, role: &str) {
assert!(
user.has_role(role),
"Expected user '{}' to have role '{}', but they don't.\nActual roles: {:?}",
user.username,
role,
user.roles
);
}
pub fn lacks_role(user: &TestUser, role: &str) {
assert!(
!user.has_role(role),
"Expected user '{}' to NOT have role '{}', but they do.\nActual roles: {:?}",
user.username,
role,
user.roles
);
}
pub fn is_authenticated(session: &MockSession) {
assert!(
session.is_authenticated(),
"Expected session to be authenticated, but it's not."
);
}
pub fn is_anonymous(session: &MockSession) {
assert!(
!session.is_authenticated(),
"Expected session to be anonymous, but it's authenticated."
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_user_anonymous() {
let user = TestUser::anonymous();
assert!(!user.is_authenticated);
assert!(user.permissions.is_empty());
assert!(user.roles.is_empty());
}
#[test]
fn test_user_authenticated() {
let user = TestUser::authenticated("alice");
assert!(user.is_authenticated);
assert_eq!(user.username, "alice");
assert!(user.email.contains("alice"));
}
#[test]
fn test_user_admin() {
let admin = TestUser::admin();
assert!(admin.is_authenticated);
assert!(admin.has_permission("admin"));
assert!(admin.has_permission("anything")); assert!(admin.has_role("admin"));
}
#[test]
fn test_user_permissions() {
let user = TestUser::authenticated("bob")
.with_permission("read")
.with_permission("write");
assert!(user.has_permission("read"));
assert!(user.has_permission("write"));
assert!(!user.has_permission("admin"));
}
#[test]
fn test_session_anonymous() {
let session = MockSession::anonymous();
assert!(!session.is_authenticated());
assert!(session.is_valid());
}
#[test]
fn test_session_authenticated() {
let user = TestUser::authenticated("alice");
let session = MockSession::authenticated(user);
assert!(session.is_authenticated());
assert!(session.user_id().is_some());
}
#[test]
fn test_session_data() {
let mut session = MockSession::anonymous();
session.set("key", serde_json::json!("value"));
let value: Option<String> = session.get("key");
assert_eq!(value, Some("value".to_string()));
}
#[test]
fn test_session_csrf() {
let session = MockSession::anonymous().with_csrf_token("test-token");
assert!(session.verify_csrf("test-token"));
assert!(!session.verify_csrf("wrong-token"));
assert!(!session.verify_csrf(""));
}
#[test]
fn test_session_expiration() {
let expired = MockSession::anonymous().expires_in(-100);
assert!(expired.is_expired());
assert!(!expired.is_valid());
let valid = MockSession::anonymous().expires_in(3600);
assert!(!valid.is_expired());
assert!(valid.is_valid());
}
#[test]
fn test_token_claims() {
let user = TestUser::authenticated("alice");
let claims = TestTokenClaims::for_user(&user)
.expires_in(3600)
.with_issuer("test-issuer")
.with_claim("role", "user");
assert_eq!(claims.sub, user.id.to_string());
assert!(!claims.is_expired());
assert_eq!(claims.iss, Some("test-issuer".to_string()));
}
}