ferro_rs/tenant/
worker.rs1use crate::tenant::context::{tenant_scope, with_tenant_scope};
4use crate::tenant::TenantLookup;
5use async_trait::async_trait;
6use ferro_queue::{Error as QueueError, TenantScopeProvider};
7use std::future::Future;
8use std::pin::Pin;
9use std::sync::Arc;
10
11pub struct FrameworkTenantScopeProvider {
17 lookup: Arc<dyn TenantLookup>,
18}
19
20impl FrameworkTenantScopeProvider {
21 pub fn new(lookup: Arc<dyn TenantLookup>) -> Self {
23 Self { lookup }
24 }
25}
26
27#[async_trait]
28impl TenantScopeProvider for FrameworkTenantScopeProvider {
29 async fn with_scope(
30 &self,
31 tenant_id: i64,
32 f: Pin<Box<dyn Future<Output = Result<(), QueueError>> + Send>>,
33 ) -> Result<(), QueueError> {
34 let tenant = self
35 .lookup
36 .find_by_id(tenant_id)
37 .await
38 .ok_or_else(|| QueueError::tenant_not_found(tenant_id))?;
39
40 let scope = tenant_scope();
41 {
42 let mut guard = scope.write().await;
43 *guard = Some(tenant);
44 }
45 with_tenant_scope(scope, f).await
46 }
47}
48
49#[cfg(test)]
50mod tests {
51 use super::*;
52 use crate::tenant::context::current_tenant;
53 use crate::tenant::TenantContext;
54
55 fn make_tenant(id: i64, slug: &str) -> TenantContext {
56 TenantContext {
57 id,
58 slug: slug.to_string(),
59 name: format!("Tenant {slug}"),
60 plan: None,
61 #[cfg(feature = "stripe")]
62 subscription: None,
63 }
64 }
65
66 struct MockLookup {
67 tenant: Option<TenantContext>,
68 }
69
70 impl MockLookup {
71 fn returning(tenant: TenantContext) -> Self {
72 Self {
73 tenant: Some(tenant),
74 }
75 }
76
77 fn not_found() -> Self {
78 Self { tenant: None }
79 }
80 }
81
82 #[async_trait]
83 impl TenantLookup for MockLookup {
84 async fn find_by_slug(&self, _slug: &str) -> Option<TenantContext> {
85 self.tenant.clone()
86 }
87
88 async fn find_by_id(&self, _id: i64) -> Option<TenantContext> {
89 self.tenant.clone()
90 }
91 }
92
93 #[tokio::test]
96 async fn with_scope_restores_tenant_context() {
97 let tenant = make_tenant(1, "acme");
98 let lookup = Arc::new(MockLookup::returning(tenant.clone()));
99 let provider = FrameworkTenantScopeProvider::new(lookup);
100
101 let result = provider.with_scope(1, Box::pin(async { Ok(()) })).await;
102
103 assert!(result.is_ok(), "Expected Ok(()), got: {result:?}");
104 }
105
106 #[tokio::test]
109 async fn with_scope_returns_tenant_not_found_for_unknown_id() {
110 let lookup = Arc::new(MockLookup::not_found());
111 let provider = FrameworkTenantScopeProvider::new(lookup);
112
113 let result = provider.with_scope(999, Box::pin(async { Ok(()) })).await;
114
115 assert!(result.is_err(), "Expected Err for unknown tenant id");
116 assert!(
117 matches!(result, Err(QueueError::TenantNotFound { tenant_id: 999 })),
118 "Expected TenantNotFound(999), got: {result:?}"
119 );
120 }
121
122 #[tokio::test]
124 async fn current_tenant_accessible_inside_scope() {
125 let tenant = make_tenant(42, "beta");
126 let lookup = Arc::new(MockLookup::returning(tenant.clone()));
127 let provider = FrameworkTenantScopeProvider::new(lookup);
128
129 let observed_tenant = Arc::new(tokio::sync::Mutex::new(None::<TenantContext>));
130 let observed_clone = observed_tenant.clone();
131
132 let result = provider
133 .with_scope(
134 42,
135 Box::pin(async move {
136 *observed_clone.lock().await = current_tenant();
137 Ok(())
138 }),
139 )
140 .await;
141
142 assert!(result.is_ok());
143 let observed = observed_tenant.lock().await;
144 assert!(
145 observed.is_some(),
146 "current_tenant() must return Some inside scope"
147 );
148 let t = observed.as_ref().unwrap();
149 assert_eq!(t.id, 42);
150 assert_eq!(t.slug, "beta");
151 }
152}