Skip to main content

camel_test/
security_fixture.rs

1//! Deterministic security config fixture for kernel E2E tests
2//! (`unify-transport-auth`, Task 1.10).
3//!
4//! Builds a `SecurityConfig` whose native section carries exactly one
5//! inline (plaintext) static credential — no `{{env:}}` placeholders, no
6//! network, fully deterministic. Feeds `resolve_authenticators` in E2E
7//! tests and mirrors the store the CLI builds from the same config.
8//!
9//! Lockstep chain (ADR-0055 store synthesis): update together with
10//! `native_authenticator` in `crates/camel-cli/src/security.rs` and
11//! `native_store_from_config` in
12//! `crates/camel-test/tests/auth_multi_credential_test.rs`.
13
14use std::sync::Arc;
15
16use camel_auth::native_auth::{NativeCredential, NativeCredentialSecret, NativeCredentialStore};
17use camel_auth::{Principal, ProviderEntry, ProviderRegistry, StaticTokenAuthenticator};
18use camel_config::config::{NativeAuthConfig, NativeCredentialEntry, SecurityConfig};
19
20/// Fixture with a single static provider named `name`.
21///
22/// - token: `test-token-<name>`
23/// - subject: `test-user-<name>`
24/// - roles: `["test-role"]`
25pub struct SecurityConfigFixture {
26    name: String,
27}
28
29impl SecurityConfigFixture {
30    pub fn single_static_provider(name: &str) -> Self {
31        Self {
32            name: name.to_string(),
33        }
34    }
35
36    fn subject(&self) -> String {
37        format!("test-user-{}", self.name)
38    }
39
40    fn token(&self) -> String {
41        format!("test-token-{}", self.name) // allow-secret
42    }
43
44    /// The fixture's `SecurityConfig` (concrete type — feeds
45    /// `resolve_authenticators` in E2E tests).
46    pub fn to_config(&self) -> SecurityConfig {
47        SecurityConfig {
48            native: Some(NativeAuthConfig {
49                subject: self.subject(),
50                issuer: None,
51                bearer_token: None,
52                api_key: None,
53                roles: vec![],
54                scopes: vec![],
55                credentials: vec![NativeCredentialEntry {
56                    subject: self.subject(),
57                    secret_env: None,
58                    secret: Some(self.token()),
59                    roles: vec!["test-role".to_string()],
60                    scopes: vec![],
61                }],
62            }),
63            ..Default::default()
64        }
65    }
66
67    /// Convenience: a `ProviderRegistry` holding the fixture's static
68    /// authenticator registered under `name` — the same store the CLI
69    /// builds from [`Self::to_config`].
70    pub fn providers(&self) -> ProviderRegistry {
71        let credential = NativeCredential {
72            secret: NativeCredentialSecret::Plaintext {
73                value: self.token().into(),
74            },
75            principal: Principal {
76                subject: self.subject(),
77                issuer: "native".into(),
78                audience: vec![],
79                scopes: vec![],
80                roles: vec!["test-role".into()],
81                claims: serde_json::Value::Object(Default::default()),
82            },
83        };
84        let store = NativeCredentialStore::try_new(vec![credential])
85            .expect("fixture credential is structurally valid"); // allow-unwrap
86        let registry = ProviderRegistry::new();
87        registry.register(
88            &self.name,
89            ProviderEntry {
90                authenticator: Arc::new(StaticTokenAuthenticator::new(store)),
91                audience_binding: None,
92            },
93        );
94        registry
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn fixture_serializes_without_env_placeholders() {
104        let fixture = SecurityConfigFixture::single_static_provider("idp-test");
105        let toml = toml::to_string(&fixture.to_config().native.expect("native section"))
106            .expect("serializes"); // allow-unwrap
107        assert!(
108            toml.contains("test-token-idp-test"),
109            "inline token must serialize for inspection: {toml}"
110        );
111        assert!(
112            !toml.contains("{{env:"),
113            "no env placeholders in a deterministic fixture: {toml}"
114        );
115    }
116
117    #[tokio::test]
118    async fn fixture_principal_shape_matches_cli_mapping() {
119        // Pins the lockstep contract: same principal the CLI's
120        // `native_principal` builds from the fixture config.
121        let fixture = SecurityConfigFixture::single_static_provider("idp-test");
122        let registry = fixture.providers();
123        let entry = registry.resolve("idp-test").expect("registered"); // allow-unwrap
124        let principal = entry
125            .authenticator
126            .authenticate_bearer("test-token-idp-test")
127            .await
128            .expect("fixture token authenticates"); // allow-unwrap
129        assert_eq!(principal.subject, "test-user-idp-test");
130        assert_eq!(principal.issuer, "native");
131        assert_eq!(principal.roles, vec!["test-role".to_string()]);
132        assert_eq!(principal.claims, serde_json::json!({}));
133    }
134
135    #[test]
136    fn fixture_registry_resolves() {
137        let fixture = SecurityConfigFixture::single_static_provider("idp-test");
138        let registry = fixture.providers();
139        assert!(registry.resolve("idp-test").is_some());
140        assert!(registry.resolve("ghost").is_none());
141    }
142}