use super::config::MockValidatorConfig;
use super::traits::{AuthContext, TokenValidator};
use crate::error::Result;
use async_trait::async_trait;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct MockValidator {
user_id: String,
tenant_id: Option<String>,
scopes: Vec<String>,
client_id: Option<String>,
claims: HashMap<String, serde_json::Value>,
always_authenticated: bool,
}
impl MockValidator {
pub fn new(user_id: impl Into<String>) -> Self {
Self {
user_id: user_id.into(),
tenant_id: None,
scopes: vec!["read".to_string(), "write".to_string()],
client_id: Some("mock-client".to_string()),
claims: HashMap::new(),
always_authenticated: true,
}
}
pub fn from_config(config: MockValidatorConfig) -> Self {
let mut claims = HashMap::new();
if let Some(obj) = config.claims.as_object() {
for (key, value) in obj {
claims.insert(key.clone(), value.clone());
}
}
if let Some(ref tenant_id) = config.default_tenant_id {
claims.insert(
"tenant_id".to_string(),
serde_json::Value::String(tenant_id.clone()),
);
}
Self {
user_id: config.default_user_id,
tenant_id: config.default_tenant_id,
scopes: config.default_scopes,
client_id: config.default_client_id,
claims,
always_authenticated: config.always_authenticated,
}
}
pub fn with_tenant_id(mut self, tenant_id: impl Into<String>) -> Self {
let tenant = tenant_id.into();
self.tenant_id = Some(tenant.clone());
self.claims
.insert("tenant_id".to_string(), serde_json::Value::String(tenant));
self
}
pub fn with_scopes<I, S>(mut self, scopes: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.scopes = scopes.into_iter().map(Into::into).collect();
self
}
pub fn with_client_id(mut self, client_id: impl Into<String>) -> Self {
self.client_id = Some(client_id.into());
self
}
pub fn with_claim(
mut self,
key: impl Into<String>,
value: impl Into<serde_json::Value>,
) -> Self {
self.claims.insert(key.into(), value.into());
self
}
pub fn require_token(mut self) -> Self {
self.always_authenticated = false;
self
}
fn build_context(&self, token: Option<&str>) -> AuthContext {
let mut claims = self.claims.clone();
if !claims.contains_key("email") {
claims.insert(
"email".to_string(),
serde_json::Value::String(format!("{}@mock.local", self.user_id)),
);
}
if !claims.contains_key("name") {
claims.insert(
"name".to_string(),
serde_json::Value::String(format!("Mock User {}", self.user_id)),
);
}
AuthContext {
subject: self.user_id.clone(),
scopes: self.scopes.clone(),
claims,
token: token.map(String::from),
client_id: self.client_id.clone(),
expires_at: Some(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
+ 3600,
),
authenticated: true,
}
}
}
impl Default for MockValidator {
fn default() -> Self {
Self::new("mock-user")
}
}
#[async_trait]
impl TokenValidator for MockValidator {
async fn validate(&self, token: &str) -> Result<AuthContext> {
if !self.always_authenticated && token.is_empty() {
return Err(crate::error::Error::protocol(
crate::error::ErrorCode::AUTHENTICATION_REQUIRED,
"Token required",
));
}
Ok(self.build_context(Some(token)))
}
}
#[derive(Debug, Default)]
pub struct MockAuthContextBuilder {
user_id: String,
tenant_id: Option<String>,
scopes: Vec<String>,
client_id: Option<String>,
claims: HashMap<String, serde_json::Value>,
token: Option<String>,
}
impl MockAuthContextBuilder {
pub fn new() -> Self {
Self {
user_id: "mock-user".to_string(),
scopes: vec!["read".to_string(), "write".to_string()],
client_id: Some("mock-client".to_string()),
..Default::default()
}
}
pub fn user_id(mut self, user_id: impl Into<String>) -> Self {
self.user_id = user_id.into();
self
}
pub fn tenant_id(mut self, tenant_id: impl Into<String>) -> Self {
let tenant = tenant_id.into();
self.tenant_id = Some(tenant.clone());
self.claims
.insert("tenant_id".to_string(), serde_json::Value::String(tenant));
self
}
pub fn scopes<I, S>(mut self, scopes: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.scopes = scopes.into_iter().map(Into::into).collect();
self
}
pub fn client_id(mut self, client_id: impl Into<String>) -> Self {
self.client_id = Some(client_id.into());
self
}
pub fn claim(mut self, key: impl Into<String>, value: impl Into<serde_json::Value>) -> Self {
self.claims.insert(key.into(), value.into());
self
}
pub fn token(mut self, token: impl Into<String>) -> Self {
self.token = Some(token.into());
self
}
pub fn build(self) -> AuthContext {
let mut claims = self.claims;
if !claims.contains_key("email") {
claims.insert(
"email".to_string(),
serde_json::Value::String(format!("{}@mock.local", self.user_id)),
);
}
AuthContext {
subject: self.user_id,
scopes: self.scopes,
claims,
token: self.token,
client_id: self.client_id,
expires_at: Some(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
+ 3600,
),
authenticated: true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_mock_validator_basic() {
let validator = MockValidator::new("test-user");
let auth = validator.validate("any-token").await.unwrap();
assert_eq!(auth.user_id(), "test-user");
assert!(auth.authenticated);
assert!(auth.has_scope("read"));
assert!(auth.has_scope("write"));
}
#[tokio::test]
async fn test_mock_validator_with_tenant() {
let validator = MockValidator::new("test-user").with_tenant_id("tenant-123");
let auth = validator.validate("token").await.unwrap();
assert_eq!(auth.tenant_id(), Some("tenant-123"));
}
#[tokio::test]
async fn test_mock_validator_with_claims() {
let validator = MockValidator::new("test-user")
.with_claim("email", "test@example.com")
.with_claim("roles", serde_json::json!(["admin"]));
let auth = validator.validate("token").await.unwrap();
assert_eq!(auth.email(), Some("test@example.com"));
let roles: Option<Vec<String>> = auth.claim("roles");
assert_eq!(roles, Some(vec!["admin".to_string()]));
}
#[tokio::test]
async fn test_mock_validator_custom_scopes() {
let validator =
MockValidator::new("test-user").with_scopes(vec!["custom:read", "custom:write"]);
let auth = validator.validate("token").await.unwrap();
assert!(auth.has_scope("custom:read"));
assert!(auth.has_scope("custom:write"));
assert!(!auth.has_scope("read")); }
#[tokio::test]
async fn test_mock_validator_require_token() {
let validator = MockValidator::new("test-user").require_token();
let result = validator.validate("").await;
assert!(result.is_err());
let result = validator.validate("some-token").await;
assert!(result.is_ok());
}
#[test]
fn test_mock_auth_context_builder() {
let auth = MockAuthContextBuilder::new()
.user_id("builder-user")
.tenant_id("builder-tenant")
.scopes(vec!["scope1", "scope2"])
.claim("custom", "value")
.build();
assert_eq!(auth.user_id(), "builder-user");
assert_eq!(auth.tenant_id(), Some("builder-tenant"));
assert!(auth.has_scope("scope1"));
assert!(auth.has_scope("scope2"));
let custom: Option<String> = auth.claim("custom");
assert_eq!(custom, Some("value".to_string()));
}
#[test]
fn test_from_config() {
let config = MockValidatorConfig {
default_user_id: "config-user".to_string(),
default_tenant_id: Some("config-tenant".to_string()),
default_scopes: vec!["read".to_string()],
default_client_id: Some("config-client".to_string()),
claims: serde_json::json!({"custom": "claim"}),
always_authenticated: true,
};
let validator = MockValidator::from_config(config);
assert_eq!(validator.user_id, "config-user");
assert_eq!(validator.tenant_id, Some("config-tenant".to_string()));
assert_eq!(validator.scopes, vec!["read".to_string()]);
}
}