Skip to main content

reinhardt_testkit/auth/
builder.rs

1use std::sync::Arc;
2use std::time::Duration;
3
4use uuid::Uuid;
5
6use super::error::TestAuthError;
7use super::identity::SessionIdentity;
8use super::secondary::SecondaryAuth;
9use super::secondary::TotpSecondaryAuth;
10use super::traits::ForceLoginUser;
11use crate::client::APIClient;
12
13/// Entry point for building auth configurations on an [`APIClient`].
14///
15/// Returned by [`APIClient::auth()`].
16pub struct AuthBuilder<'a> {
17	client: &'a APIClient,
18}
19
20impl<'a> AuthBuilder<'a> {
21	/// Create a new `AuthBuilder` for the given client.
22	pub(crate) fn new(client: &'a APIClient) -> Self {
23		Self { client }
24	}
25
26	/// Configure session-based authentication.
27	///
28	/// Creates a real session in the `AsyncSessionBackend` and sets the
29	/// `sessionid` cookie on the client. The session will be validated by
30	/// `CookieSessionAuthMiddleware` on subsequent requests.
31	pub fn session(
32		self,
33		user: &impl ForceLoginUser,
34		backend: Arc<dyn reinhardt_middleware::session::AsyncSessionBackend>,
35	) -> SessionAuthBuilder<'a> {
36		SessionAuthBuilder {
37			client: self.client,
38			identity: SessionIdentity::from_user(user),
39			backend,
40			ttl: Duration::from_secs(30 * 60),
41			secondary: vec![],
42		}
43	}
44
45	/// Configure JWT Bearer token authentication.
46	pub fn jwt(self, user: &impl ForceLoginUser, config: JwtTestConfig) -> JwtAuthBuilder<'a> {
47		JwtAuthBuilder {
48			client: self.client,
49			identity: SessionIdentity::from_user(user),
50			config,
51			secondary: vec![],
52		}
53	}
54}
55
56/// Builder for session-based test authentication.
57///
58/// Created by [`AuthBuilder::session()`].
59pub struct SessionAuthBuilder<'a> {
60	client: &'a APIClient,
61	identity: SessionIdentity,
62	backend: Arc<dyn reinhardt_middleware::session::AsyncSessionBackend>,
63	ttl: Duration,
64	secondary: Vec<Box<dyn SecondaryAuth>>,
65}
66
67impl<'a> SessionAuthBuilder<'a> {
68	/// Override the `is_staff` flag.
69	///
70	/// Use this when the user's `ForceLoginUser` impl defaults `is_staff` to `false`
71	/// but the test requires staff access.
72	pub fn with_staff(mut self, is_staff: bool) -> Self {
73		self.identity.is_staff = is_staff;
74		self
75	}
76
77	/// Override the `is_superuser` flag.
78	pub fn with_superuser(mut self, is_superuser: bool) -> Self {
79		self.identity.is_superuser = is_superuser;
80		self
81	}
82
83	/// Set the session TTL. Default: 30 minutes.
84	pub fn with_ttl(mut self, ttl: Duration) -> Self {
85		self.ttl = ttl;
86		self
87	}
88
89	/// Add TOTP MFA as a secondary auth layer.
90	///
91	/// Uses a pre-generated TOTP code. Generate it from the user's registered
92	/// secret before calling this method.
93	pub fn with_mfa_code(self, code: impl Into<String>) -> Self {
94		self.with_secondary(TotpSecondaryAuth::with_code_only(code))
95	}
96
97	/// Add a custom secondary auth layer.
98	pub fn with_secondary(mut self, auth: impl SecondaryAuth + 'static) -> Self {
99		self.secondary.push(Box::new(auth));
100		self
101	}
102
103	/// Apply the authentication configuration to the client.
104	///
105	/// This creates a real session in the backend, sets the `sessionid` cookie,
106	/// and applies any secondary auth layers.
107	pub async fn apply(self) -> Result<(), TestAuthError> {
108		// 1. Generate session ID
109		let session_id = Uuid::now_v7().to_string();
110
111		// 2. Build SessionData from identity
112		let session_data = self.identity.to_session_data(&session_id, self.ttl);
113
114		// 3. Save to AsyncSessionBackend
115		self.backend
116			.save(&session_data)
117			.await
118			.map_err(|e| TestAuthError::SessionError(e.to_string()))?;
119
120		// 4. Set sessionid cookie on client
121		self.client
122			.set_cookie("sessionid", &session_id)
123			.await
124			.map_err(|e| TestAuthError::ClientError(e.to_string()))?;
125
126		// 5. Apply secondary auth layers
127		for secondary in &self.secondary {
128			secondary
129				.apply_to_client(self.client, &self.identity)
130				.await?;
131		}
132
133		Ok(())
134	}
135}
136
137/// Builder for JWT test authentication.
138///
139/// Created by [`AuthBuilder::jwt()`].
140pub struct JwtAuthBuilder<'a> {
141	client: &'a APIClient,
142	identity: SessionIdentity,
143	config: JwtTestConfig,
144	secondary: Vec<Box<dyn SecondaryAuth>>,
145}
146
147impl<'a> JwtAuthBuilder<'a> {
148	/// Add a custom secondary auth layer.
149	pub fn with_secondary(mut self, auth: impl SecondaryAuth + 'static) -> Self {
150		self.secondary.push(Box::new(auth));
151		self
152	}
153
154	/// Apply JWT authentication to the client.
155	///
156	/// Signs a JWT with the configured secret and sets the `Authorization: Bearer` header.
157	pub async fn apply(self) -> Result<(), TestAuthError> {
158		use reinhardt_auth::jwt::{Claims, JwtAuth};
159
160		// Build claims
161		let claims = Claims::new(
162			self.identity.user_id.clone(),
163			self.identity.user_id.clone(),
164			chrono::Duration::seconds(self.config.expiry.as_secs() as i64),
165			self.identity.is_staff,
166			self.identity.is_superuser,
167		);
168
169		// Sign JWT
170		let jwt_auth = JwtAuth::new(self.config.secret.as_bytes());
171		let token = jwt_auth
172			.encode(&claims)
173			.map_err(|e| TestAuthError::JwtError(e.to_string()))?;
174
175		// Set Authorization header
176		self.client
177			.set_header("Authorization", &format!("Bearer {token}"))
178			.await
179			.map_err(|e| TestAuthError::ClientError(e.to_string()))?;
180
181		// Apply secondary auth layers
182		for secondary in &self.secondary {
183			secondary
184				.apply_to_client(self.client, &self.identity)
185				.await?;
186		}
187
188		Ok(())
189	}
190}
191
192/// JWT configuration for test contexts.
193#[derive(Clone, Debug)]
194pub struct JwtTestConfig {
195	/// Secret key for signing JWTs.
196	pub secret: String,
197	/// Token expiry duration. Default: 1 hour.
198	pub expiry: Duration,
199}
200
201impl Default for JwtTestConfig {
202	fn default() -> Self {
203		Self {
204			secret: "test-secret-key-for-testing-only".into(),
205			expiry: Duration::from_secs(3600),
206		}
207	}
208}