//! Axum extractor for the per-request `TenantContext`. //! //! Handlers consume it as a normal extractor argument: //! //! ```ignore //! async fn list_findings(TenantCtx(ctx): TenantCtx) -> Json<...> { //! let filter = compliance_core::db::tenant_filter(&ctx); //! ... //! } //! ``` //! //! The middleware ([`crate::auth::require_jwt_auth`]) is responsible for //! inserting the context into the request extensions. If it's missing on //! a route that uses this extractor, that's a bug in the wiring — we //! return 401 so the caller sees an auth failure rather than a 500. use axum::{ extract::FromRequestParts, http::{request::Parts, StatusCode}, response::{IntoResponse, Response}, }; use crate::TenantContext; #[derive(Debug, Clone)] pub struct TenantCtx(pub TenantContext); #[derive(Debug)] pub struct TenantCtxRejection; impl IntoResponse for TenantCtxRejection { fn into_response(self) -> Response { ( StatusCode::UNAUTHORIZED, "Missing tenant context — request was not authenticated", ) .into_response() } } impl FromRequestParts for TenantCtx where S: Send + Sync, { type Rejection = TenantCtxRejection; async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { parts .extensions .get::() .cloned() .map(TenantCtx) .ok_or(TenantCtxRejection) } } #[cfg(test)] #[allow(clippy::expect_used, clippy::unwrap_used)] mod tests { use super::*; use crate::TenantStatus; use axum::http::Request; fn ctx() -> TenantContext { TenantContext { tenant_id: "t-1".to_string(), tenant_slug: "acme".to_string(), org_roles: vec![], products: vec![], plan: "starter".to_string(), status: TenantStatus::Active, user_id: "u-1".to_string(), user_name: None, } } #[tokio::test] async fn extracts_context_when_present() { let mut req = Request::new(()); req.extensions_mut().insert(ctx()); let (mut parts, _) = req.into_parts(); let TenantCtx(found) = TenantCtx::from_request_parts(&mut parts, &()) .await .expect("extractor should succeed"); assert_eq!(found.tenant_id, "t-1"); } #[tokio::test] async fn rejects_when_missing() { let req: Request<()> = Request::new(()); let (mut parts, _) = req.into_parts(); let err = TenantCtx::from_request_parts(&mut parts, &()).await; assert!(err.is_err()); } }