Skip to main content

af_context/
testing.rs

1//! Test doubles for the context ports: a provider that resolves one fixed
2//! identity for every request.
3
4use std::collections::BTreeSet;
5
6use async_trait::async_trait;
7
8use crate::{
9    ContextError, PlatformContextProvider, RequestContext, RequestId, RequestMetadata,
10    ScheduledContextProvider, SubjectId, TenantId,
11};
12
13/// Resolves every transport request and scheduled job to one tenant/subject.
14/// `x-locale` metadata overrides the default locale; everything else is ignored.
15#[derive(Debug, Clone)]
16pub struct StaticContextProvider {
17    /// Tenant that owns this record.
18    pub tenant_id: TenantId,
19    /// Subject (user or service principal) acting on or owning this record.
20    pub subject_id: SubjectId,
21    /// Roles granted to the subject.
22    pub roles: BTreeSet<String>,
23    /// Product entitlements granted to the subject.
24    pub entitlements: BTreeSet<String>,
25    /// BCP 47 locale for user-facing text.
26    pub locale: String,
27}
28
29impl StaticContextProvider {
30    /// A provider that answers every request as `tenant_id` / `subject_id`
31    /// with no roles or entitlements.
32    ///
33    /// # Panics
34    ///
35    /// When either identifier is blank; a test double with an invalid
36    /// identity is a test bug, not a runtime condition.
37    pub fn new(tenant_id: &str, subject_id: &str) -> Self {
38        Self {
39            tenant_id: tenant_id.parse().expect("StaticContextProvider tenant_id"),
40            subject_id: subject_id
41                .parse()
42                .expect("StaticContextProvider subject_id"),
43            roles: BTreeSet::new(),
44            entitlements: BTreeSet::new(),
45            locale: "en".into(),
46        }
47    }
48
49    /// Grant these roles to every resolved context.
50    pub fn with_roles(mut self, roles: impl IntoIterator<Item = impl Into<String>>) -> Self {
51        self.roles = roles.into_iter().map(Into::into).collect();
52        self
53    }
54
55    /// Grant these entitlements to every resolved context.
56    pub fn with_entitlements(
57        mut self,
58        entitlements: impl IntoIterator<Item = impl Into<String>>,
59    ) -> Self {
60        self.entitlements = entitlements.into_iter().map(Into::into).collect();
61        self
62    }
63
64    fn context(&self, request_id: RequestId, locale: &str) -> RequestContext {
65        RequestContext {
66            tenant_id: self.tenant_id.clone(),
67            subject_id: self.subject_id.clone(),
68            roles: self.roles.clone(),
69            locale: locale.to_owned(),
70            request_id,
71            entitlements: self.entitlements.clone(),
72        }
73    }
74}
75
76#[async_trait]
77impl PlatformContextProvider for StaticContextProvider {
78    async fn resolve(&self, metadata: &RequestMetadata) -> Result<RequestContext, ContextError> {
79        let locale = metadata
80            .0
81            .get("x-locale")
82            .map(String::as_str)
83            .unwrap_or(&self.locale);
84        let request_id = metadata
85            .0
86            .get("x-request-id")
87            .cloned()
88            .unwrap_or_else(|| format!("static-{}", self.roles.len()))
89            .parse()
90            .map_err(|error: crate::EmptyId| ContextError::Rejected(error.to_string()))?;
91        Ok(self.context(request_id, locale))
92    }
93}
94
95#[async_trait]
96impl ScheduledContextProvider for StaticContextProvider {
97    async fn resolve_scheduled(
98        &self,
99        tenant_id: &TenantId,
100        subject_id: &SubjectId,
101        request_id: &RequestId,
102        locale: &str,
103    ) -> Result<RequestContext, ContextError> {
104        if tenant_id != &self.tenant_id || subject_id != &self.subject_id {
105            return Err(ContextError::Rejected(format!(
106                "static provider only serves {}/{}",
107                self.tenant_id, self.subject_id
108            )));
109        }
110        Ok(self.context(request_id.clone(), locale))
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[tokio::test]
119    async fn static_provider_serves_one_identity() {
120        let provider = StaticContextProvider::new("tenant", "subject").with_roles(["admin"]);
121        let resolved = provider.resolve(&RequestMetadata::default()).await.unwrap();
122        assert_eq!(resolved.tenant_id, "tenant");
123        assert!(resolved.roles.contains("admin"));
124        assert!(provider
125            .resolve_scheduled(
126                &"other".parse().unwrap(),
127                &"subject".parse().unwrap(),
128                &"r".parse().unwrap(),
129                "en",
130            )
131            .await
132            .is_err());
133    }
134}