use road_runner_common::auth::{PrincipalKind, UserContext};
use road_runner_common::error::AppError;
use uuid::Uuid;
fn context() -> UserContext {
UserContext {
subject: "11111111-1111-1111-1111-111111111111".to_string(),
principal: PrincipalKind::User,
email: Some("user@example.com".to_string()),
username: Some("user".to_string()),
roles: vec!["trader".to_string(), "risk_admin".to_string()],
scopes: vec!["read".to_string(), "spot".to_string()],
capabilities: vec!["account.read".to_string()],
api_key_id: None,
acr: Some("aal2".to_string()),
}
}
#[test]
fn principal_kind_parse_accepts_aliases_and_defaults_to_user() {
assert_eq!(PrincipalKind::default(), PrincipalKind::User);
assert_eq!(PrincipalKind::parse("apikey"), PrincipalKind::ApiKey);
assert_eq!(PrincipalKind::parse("api_key"), PrincipalKind::ApiKey);
assert_eq!(PrincipalKind::parse("api-key"), PrincipalKind::ApiKey);
assert_eq!(PrincipalKind::parse(" service "), PrincipalKind::Service);
assert_eq!(PrincipalKind::parse("unknown"), PrincipalKind::User);
}
#[test]
fn principal_kind_as_str_returns_wire_values() {
assert_eq!(PrincipalKind::User.as_str(), "user");
assert_eq!(PrincipalKind::ApiKey.as_str(), "apikey");
assert_eq!(PrincipalKind::Service.as_str(), "service");
}
#[test]
fn user_id_parses_uuid_and_user_id_opt_matches() {
let ctx = context();
let parsed = ctx.user_id().unwrap();
assert_eq!(parsed, Uuid::parse_str(&ctx.subject).unwrap());
assert_eq!(ctx.user_id_opt(), Some(parsed));
}
#[test]
fn user_id_returns_unauthorized_for_non_uuid_subject() {
let mut ctx = context();
ctx.subject = "service-account".to_string();
let result = ctx.user_id();
assert!(matches!(result, Err(AppError::Unauthorized { message }) if message == "subject is not a valid user id"));
assert_eq!(ctx.user_id_opt(), None);
}
#[test]
fn principal_type_helpers_identify_context_kind() {
let mut ctx = context();
assert!(ctx.is_user());
ctx.principal = PrincipalKind::ApiKey;
assert!(ctx.is_api_key());
ctx.principal = PrincipalKind::Service;
assert!(ctx.is_service());
}
#[test]
fn role_checks_cover_success_failure_and_admin_suffix() {
let ctx = context();
assert!(ctx.has_role("trader"));
assert!(ctx.has_any_role(&["viewer", "trader"]));
assert!(ctx.require_role("trader").is_ok());
assert!(ctx.require_any_role(&["viewer", "risk_admin"]).is_ok());
assert!(ctx.is_admin());
assert!(ctx.require_admin().is_ok());
assert!(matches!(ctx.require_role("missing"), Err(AppError::Forbidden { message }) if message == "Missing role: missing"));
assert!(matches!(
ctx.require_any_role(&["viewer", "ops"]),
Err(AppError::Forbidden { message }) if message == "Missing any of roles: viewer, ops"
));
}
#[test]
fn admin_requires_admin_or_suffix_case_insensitively() {
let mut ctx = context();
ctx.roles = vec!["support".to_string()];
assert!(!ctx.is_admin());
assert!(matches!(ctx.require_admin(), Err(AppError::Forbidden { message }) if message == "Admin role required"));
ctx.roles = vec!["ADMIN".to_string()];
assert!(ctx.is_admin());
}
#[test]
fn scope_checks_return_forbidden_for_missing_scope() {
let ctx = context();
assert!(ctx.has_scope("read"));
assert!(ctx.require_scope("spot").is_ok());
assert!(matches!(ctx.require_scope("withdraw"), Err(AppError::Forbidden { message }) if message == "Missing scope: withdraw"));
}
#[test]
fn capability_checks_return_forbidden_for_missing_capability() {
let ctx = context();
assert!(ctx.has_capability("account.read"));
assert!(ctx.require_capability("account.read").is_ok());
assert!(matches!(
ctx.require_capability("withdraw.crypto"),
Err(AppError::Forbidden { message }) if message == "Capability not allowed: withdraw.crypto"
));
}
#[cfg(feature = "web")]
mod web {
use super::{context, UserContext};
use actix_web::dev::Payload;
use actix_web::{test::TestRequest, FromRequest, HttpMessage};
use road_runner_common::error::AppError;
#[actix_web::test]
async fn user_context_extractor_reads_context_from_extensions() {
let req = TestRequest::default().to_http_request();
req.extensions_mut().insert(context());
let mut payload = Payload::None;
let extracted = UserContext::from_request(&req, &mut payload).await.unwrap();
assert_eq!(extracted.subject, "11111111-1111-1111-1111-111111111111");
}
#[actix_web::test]
async fn user_context_extractor_returns_unauthorized_when_missing() {
let req = TestRequest::default().to_http_request();
let mut payload = Payload::None;
let result = UserContext::from_request(&req, &mut payload).await;
assert!(matches!(result, Err(AppError::Unauthorized { message }) if message == "Missing user context"));
}
}