use std::collections::HashMap;
use axum::extract::FromRequestParts;
use chrono::Utc;
use fraiseql_core::{security::AuthenticatedUser, types::UserId};
use serde_json::json;
use super::OptionalSecurityContext;
use crate::middleware::AuthUser;
async fn context_for(user: AuthenticatedUser) -> fraiseql_core::security::SecurityContext {
let (mut parts, _body) = axum::http::Request::builder()
.body(axum::body::Body::empty())
.expect("empty request body builds")
.into_parts();
parts.extensions.insert(AuthUser(user));
let OptionalSecurityContext(ctx) = OptionalSecurityContext::from_request_parts(&mut parts, &())
.await
.expect("OptionalSecurityContext extraction is infallible here");
ctx.expect("an AuthUser in extensions yields a SecurityContext")
}
fn user_with_claims(extra_claims: HashMap<String, serde_json::Value>) -> AuthenticatedUser {
AuthenticatedUser {
user_id: UserId::new("user-1"),
scopes: vec![],
expires_at: Utc::now() + chrono::Duration::hours(1),
email: None,
display_name: None,
extra_claims,
}
}
#[tokio::test]
async fn extractor_populates_roles_from_jwt_roles_claim() {
let mut extra = HashMap::new();
extra.insert("roles".to_string(), json!(["report_reader"]));
let ctx = context_for(user_with_claims(extra)).await;
assert!(
ctx.has_role("report_reader"),
"roles must be reachable for the requires_role gate"
);
}
#[tokio::test]
async fn extractor_populates_roles_from_scalar_role_claim() {
let mut extra = HashMap::new();
extra.insert("role".to_string(), json!("admin"));
let ctx = context_for(user_with_claims(extra)).await;
assert_eq!(ctx.roles, vec!["admin".to_string()]);
}
#[tokio::test]
async fn extractor_keeps_role_claim_in_attributes_too() {
let mut extra = HashMap::new();
extra.insert("roles".to_string(), json!(["report_reader"]));
let ctx = context_for(user_with_claims(extra)).await;
assert_eq!(ctx.attributes.get("roles"), Some(&json!(["report_reader"])));
}
#[tokio::test]
async fn extractor_leaves_roles_empty_without_claim() {
let ctx = context_for(user_with_claims(HashMap::new())).await;
assert!(ctx.roles.is_empty());
}
mod shared_security_context {
use super::{HashMap, json, user_with_claims};
use crate::extractors::build_security_context;
#[test]
fn org_id_claim_becomes_the_tenant_id() {
let mut extra = HashMap::new();
extra.insert("org_id".to_string(), json!("acme"));
let ctx = build_security_context(&user_with_claims(extra), "req-1".to_string());
assert_eq!(
ctx.tenant_id.as_ref().map(|t| t.0.as_str()),
Some("acme"),
"org_id must resolve to a tenant key for per-tenant dispatch",
);
}
#[test]
fn extra_claims_are_forwarded_to_attributes() {
let mut extra = HashMap::new();
extra.insert("department".to_string(), json!("finance"));
let ctx = build_security_context(&user_with_claims(extra), "req-1".to_string());
assert_eq!(ctx.attributes.get("department"), Some(&json!("finance")));
}
#[test]
fn framework_namespaced_claims_are_not_forwarded() {
let mut extra = HashMap::new();
extra.insert("fraiseql.actor_type".to_string(), json!("system"));
let ctx = build_security_context(&user_with_claims(extra), "req-1".to_string());
assert_ne!(ctx.attributes.get("fraiseql.actor_type"), Some(&json!("system")));
}
#[test]
fn no_org_id_claim_leaves_the_tenant_unset() {
let ctx = build_security_context(&user_with_claims(HashMap::new()), "req-1".to_string());
assert!(ctx.tenant_id.is_none());
}
}