use chrono::{Duration, Utc};
use pulseengine_mcp_auth::oauth::{
models::{AuthorizationCode, OAuthClient, OAuthError, RefreshToken},
pkce::{validate_code_challenge, validate_code_verifier, verify_pkce},
storage::{InMemoryOAuthStorage, OAuthStorage, OAuthStorageError},
};
#[test]
fn test_verify_pkce() {
let verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
let challenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM";
assert!(verify_pkce(verifier, challenge));
let wrong_verifier = "wrong_verifier_123456789012345678901234567890";
assert!(!verify_pkce(wrong_verifier, challenge));
}
#[test]
fn test_validate_code_verifier() {
assert!(validate_code_verifier(
"dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
));
assert!(!validate_code_verifier("short"));
let long_str = "a".repeat(129);
assert!(!validate_code_verifier(&long_str));
assert!(!validate_code_verifier(
"dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk="
));
}
#[test]
fn test_validate_code_challenge() {
assert!(validate_code_challenge(
"E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
));
assert!(!validate_code_challenge("short"));
let long_str = "a".repeat(129);
assert!(!validate_code_challenge(&long_str));
assert!(!validate_code_challenge(
"E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw.cM"
));
}
#[test]
fn test_oauth_error_creation() {
let err = OAuthError::invalid_request("Test message");
assert_eq!(err.error, "invalid_request");
assert_eq!(err.error_description.unwrap(), "Test message");
let err2 = OAuthError::unauthorized_client("Client not registered");
assert_eq!(err2.error, "unauthorized_client");
let err3 = OAuthError::invalid_grant("Code expired");
assert_eq!(err3.error, "invalid_grant");
let err4 = OAuthError::invalid_client("Bad client");
assert_eq!(err4.error, "invalid_client");
let err5 = OAuthError::unsupported_grant_type("Unsupported");
assert_eq!(err5.error, "unsupported_grant_type");
}
#[tokio::test]
async fn test_storage_save_and_get_client() {
let storage = InMemoryOAuthStorage::new();
let client = OAuthClient {
client_id: "test_client_123".to_string(),
client_secret: "secret_abc".to_string(),
client_name: "Test Client".to_string(),
redirect_uris: vec!["https://example.com/callback".to_string()],
created_at: Utc::now(),
client_secret_expires_at: None,
};
storage.save_client(&client).await.unwrap();
let retrieved = storage.get_client("test_client_123").await.unwrap();
assert_eq!(retrieved.client_id, "test_client_123");
assert_eq!(retrieved.client_name, "Test Client");
assert_eq!(retrieved.redirect_uris.len(), 1);
}
#[tokio::test]
async fn test_storage_get_nonexistent_client() {
let storage = InMemoryOAuthStorage::new();
let result = storage.get_client("non_existent").await;
assert!(result.is_err());
match result.unwrap_err() {
OAuthStorageError::ClientNotFound(_) => {}
_ => panic!("Expected ClientNotFound error"),
}
}
#[tokio::test]
async fn test_storage_verify_client_secret() {
let storage = InMemoryOAuthStorage::new();
let client = OAuthClient {
client_id: "test_client".to_string(),
client_secret: "correct_secret".to_string(),
client_name: "Test".to_string(),
redirect_uris: vec!["https://example.com/callback".to_string()],
created_at: Utc::now(),
client_secret_expires_at: None,
};
storage.save_client(&client).await.unwrap();
let result = storage
.verify_client_secret("test_client", "correct_secret")
.await
.unwrap();
assert!(result);
let result = storage
.verify_client_secret("test_client", "wrong_secret")
.await
.unwrap();
assert!(!result);
}
#[tokio::test]
async fn test_storage_authorization_code_lifecycle() {
let storage = InMemoryOAuthStorage::new();
let code = AuthorizationCode {
code: "auth_code_123".to_string(),
client_id: "test_client".to_string(),
redirect_uri: "https://example.com/callback".to_string(),
code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM".to_string(),
resource: None,
scopes: vec!["read".to_string(), "write".to_string()],
expires_at: Utc::now() + Duration::minutes(10),
created_at: Utc::now(),
};
storage.save_authorization_code(&code).await.unwrap();
let retrieved = storage
.get_authorization_code("auth_code_123")
.await
.unwrap();
assert_eq!(retrieved.code, "auth_code_123");
assert_eq!(retrieved.client_id, "test_client");
assert_eq!(retrieved.scopes.len(), 2);
storage
.delete_authorization_code("auth_code_123")
.await
.unwrap();
let result = storage.get_authorization_code("auth_code_123").await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_storage_authorization_code_expiration() {
let storage = InMemoryOAuthStorage::new();
let expired_code = AuthorizationCode {
code: "expired_code".to_string(),
client_id: "test_client".to_string(),
redirect_uri: "https://example.com/callback".to_string(),
code_challenge: "challenge".to_string(),
resource: None,
scopes: vec![],
expires_at: Utc::now() - Duration::seconds(1),
created_at: Utc::now() - Duration::minutes(1),
};
storage
.save_authorization_code(&expired_code)
.await
.unwrap();
let result = storage.get_authorization_code("expired_code").await;
assert!(result.is_err());
match result.unwrap_err() {
OAuthStorageError::CodeExpired => {}
_ => panic!("Expected CodeExpired error"),
}
}
#[tokio::test]
async fn test_storage_refresh_token_lifecycle() {
let storage = InMemoryOAuthStorage::new();
let token = RefreshToken {
token: "refresh_123".to_string(),
client_id: "test_client".to_string(),
resource: None,
scopes: vec!["read".to_string(), "write".to_string()],
expires_at: Utc::now() + Duration::days(30),
created_at: Utc::now(),
};
storage.save_refresh_token(&token).await.unwrap();
let retrieved = storage.get_refresh_token("refresh_123").await.unwrap();
assert_eq!(retrieved.token, "refresh_123");
assert_eq!(retrieved.client_id, "test_client");
assert_eq!(retrieved.scopes.len(), 2);
storage.delete_refresh_token("refresh_123").await.unwrap();
let result = storage.get_refresh_token("refresh_123").await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_storage_refresh_token_expiration() {
let storage = InMemoryOAuthStorage::new();
let expired_token = RefreshToken {
token: "expired_refresh".to_string(),
client_id: "test_client".to_string(),
resource: None,
scopes: vec![],
expires_at: Utc::now() - Duration::seconds(1),
created_at: Utc::now() - Duration::days(1),
};
storage.save_refresh_token(&expired_token).await.unwrap();
let result = storage.get_refresh_token("expired_refresh").await;
assert!(result.is_err());
match result.unwrap_err() {
OAuthStorageError::TokenExpired => {}
_ => panic!("Expected TokenExpired error"),
}
}
#[tokio::test]
async fn test_storage_cleanup_expired() {
let storage = InMemoryOAuthStorage::new();
let expired_code = AuthorizationCode {
code: "expired".to_string(),
client_id: "test".to_string(),
redirect_uri: "https://example.com/callback".to_string(),
code_challenge: "challenge".to_string(),
resource: None,
scopes: vec![],
expires_at: Utc::now() - Duration::seconds(1),
created_at: Utc::now() - Duration::minutes(1),
};
storage
.save_authorization_code(&expired_code)
.await
.unwrap();
let expired_token = RefreshToken {
token: "expired_refresh".to_string(),
client_id: "test".to_string(),
resource: None,
scopes: vec![],
expires_at: Utc::now() - Duration::seconds(1),
created_at: Utc::now() - Duration::days(1),
};
storage.save_refresh_token(&expired_token).await.unwrap();
storage.cleanup_expired().await.unwrap();
assert!(storage.get_authorization_code("expired").await.is_err());
assert!(storage.get_refresh_token("expired_refresh").await.is_err());
}
#[test]
fn test_oauth_client_model() {
let client = OAuthClient {
client_id: "test_123".to_string(),
client_secret: "secret_abc".to_string(),
client_name: "Test Client".to_string(),
redirect_uris: vec!["https://example.com/callback".to_string()],
created_at: Utc::now(),
client_secret_expires_at: None,
};
assert!(!client.redirect_uris.is_empty());
assert!(!client.client_secret.is_empty());
}
#[test]
fn test_authorization_code_model() {
let code = AuthorizationCode {
code: "test_code".to_string(),
client_id: "client_123".to_string(),
redirect_uri: "https://example.com/callback".to_string(),
code_challenge: "challenge_123".to_string(),
resource: Some("https://api.example.com".to_string()),
scopes: vec!["read".to_string(), "write".to_string()],
expires_at: Utc::now() + Duration::minutes(10),
created_at: Utc::now(),
};
assert_eq!(code.scopes.len(), 2);
assert!(code.resource.is_some());
assert!(code.expires_at > Utc::now());
}
#[test]
fn test_refresh_token_model() {
let token = RefreshToken {
token: "refresh_123".to_string(),
client_id: "client_123".to_string(),
resource: None,
scopes: vec!["read".to_string()],
expires_at: Utc::now() + Duration::days(30),
created_at: Utc::now(),
};
assert!(!token.token.is_empty());
assert_eq!(token.scopes.len(), 1);
assert!(token.expires_at > Utc::now());
}