use std::future::Future;
use std::sync::Arc;
use crate::auth::IdentityUser;
tokio::task_local! {
static IDENTITY: Option<Arc<IdentityUser>>;
}
pub struct RequestContext;
impl RequestContext {
pub async fn run<F, R>(identity: Option<Arc<IdentityUser>>, f: F) -> R
where
F: Future<Output = R>,
{
IDENTITY.scope(identity, f).await
}
pub fn identity() -> Option<Arc<IdentityUser>> {
IDENTITY
.try_with(|identity| identity.as_ref().map(Arc::clone))
.ok()
.flatten()
}
pub fn operator_id() -> Option<String> {
Self::identity().map(|identity| identity.id.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn identity(id: &str, tenant: &str) -> Arc<IdentityUser> {
Arc::new(IdentityUser {
id: id.to_string(),
tenant_id: tenant.to_string(),
..Default::default()
})
}
#[tokio::test]
async fn identity_and_operator_id_are_visible_inside_run() {
let user = identity("user-1", "acme");
RequestContext::run(Some(Arc::clone(&user)), async {
let current = RequestContext::identity().expect("identity must be in scope");
assert!(
Arc::ptr_eq(¤t, &user),
"identity() must hand back the same Arc, not a second copy"
);
assert_eq!(current.tenant(), Some("acme"));
assert_eq!(RequestContext::operator_id().as_deref(), Some("user-1"));
})
.await;
}
#[tokio::test]
async fn anonymous_scope_has_no_identity() {
RequestContext::run(None, async {
assert!(RequestContext::identity().is_none());
assert!(RequestContext::operator_id().is_none());
})
.await;
}
#[test]
fn outside_run_work_is_system() {
assert!(RequestContext::identity().is_none());
assert!(RequestContext::operator_id().is_none());
}
#[tokio::test]
async fn nested_scope_restores_the_outer_identity() {
let outer = identity("outer-user", "");
RequestContext::run(Some(Arc::clone(&outer)), async {
RequestContext::run(None, async {
assert!(RequestContext::identity().is_none());
})
.await;
assert_eq!(
RequestContext::operator_id().as_deref(),
Some("outer-user"),
"inner scope must not leak into the outer request"
);
})
.await;
}
}