Skip to main content

reinhardt_testkit/server_fn/
auth.rs

1//! Authentication and authorization mocking for server function testing.
2//!
3//! This module provides utilities for simulating authenticated users,
4//! sessions, and permissions in server function tests.
5//!
6//! # Example
7//!
8//! ```rust,ignore
9//! use reinhardt_testkit::server_fn::auth::{TestUser, MockSession};
10//!
11//! // Create an admin user
12//! let admin = TestUser::admin();
13//!
14//! // Create a user with specific permissions
15//! let user = TestUser::authenticated("alice")
16//!     .with_permission("posts:read")
17//!     .with_permission("posts:write")
18//!     .with_role("editor");
19//!
20//! // Create an authenticated session
21//! let session = MockSession::authenticated(user);
22//! ```
23
24#![cfg(native)]
25
26use std::collections::HashMap;
27
28use serde::{Deserialize, Serialize};
29use serde_json::Value;
30use uuid::Uuid;
31
32/// A test user for simulating authentication in tests.
33///
34/// This struct represents a user that can be injected into the test context
35/// to simulate authenticated requests.
36///
37/// # Example
38///
39/// ```rust,ignore
40/// // Anonymous user
41/// let anon = TestUser::anonymous();
42///
43/// // Simple authenticated user
44/// let user = TestUser::authenticated("alice");
45///
46/// // Admin user with full permissions
47/// let admin = TestUser::admin();
48///
49/// // Custom user with specific attributes
50/// let custom = TestUser::authenticated("bob")
51///     .with_email("bob@example.com")
52///     .with_permission("admin:read")
53///     .with_role("moderator");
54/// ```
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct TestUser {
57	/// Unique user identifier.
58	pub id: Uuid,
59	/// Username.
60	pub username: String,
61	/// Email address.
62	pub email: String,
63	/// List of permissions granted to this user.
64	pub permissions: Vec<String>,
65	/// List of roles assigned to this user.
66	pub roles: Vec<String>,
67	/// Whether the user is authenticated.
68	pub is_authenticated: bool,
69	/// Additional custom attributes.
70	pub attributes: HashMap<String, Value>,
71}
72
73impl Default for TestUser {
74	fn default() -> Self {
75		Self {
76			id: Uuid::now_v7(),
77			username: String::new(),
78			email: String::new(),
79			permissions: Vec::new(),
80			roles: Vec::new(),
81			is_authenticated: false,
82			attributes: HashMap::new(),
83		}
84	}
85}
86
87impl TestUser {
88	/// Create an anonymous (unauthenticated) user.
89	pub fn anonymous() -> Self {
90		Self::default()
91	}
92
93	/// Create an authenticated user with the given username.
94	pub fn authenticated(username: impl Into<String>) -> Self {
95		let username = username.into();
96		Self {
97			id: Uuid::now_v7(),
98			email: format!("{}@test.example.com", username),
99			username,
100			is_authenticated: true,
101			..Default::default()
102		}
103	}
104
105	/// Create an admin user with full permissions.
106	pub fn admin() -> Self {
107		Self {
108			id: Uuid::now_v7(),
109			username: "admin".to_string(),
110			email: "admin@test.example.com".to_string(),
111			permissions: vec![
112				"admin".to_string(),
113				"*".to_string(), // Wildcard permission
114			],
115			roles: vec!["admin".to_string(), "superuser".to_string()],
116			is_authenticated: true,
117			attributes: HashMap::new(),
118		}
119	}
120
121	/// Create a user with a specific ID.
122	pub fn with_id(mut self, id: Uuid) -> Self {
123		self.id = id;
124		self
125	}
126
127	/// Set the email address.
128	pub fn with_email(mut self, email: impl Into<String>) -> Self {
129		self.email = email.into();
130		self
131	}
132
133	/// Add a permission.
134	pub fn with_permission(mut self, permission: impl Into<String>) -> Self {
135		self.permissions.push(permission.into());
136		self
137	}
138
139	/// Add multiple permissions.
140	pub fn with_permissions<S: Into<String>>(
141		mut self,
142		permissions: impl IntoIterator<Item = S>,
143	) -> Self {
144		for perm in permissions {
145			self.permissions.push(perm.into());
146		}
147		self
148	}
149
150	/// Add a role.
151	pub fn with_role(mut self, role: impl Into<String>) -> Self {
152		self.roles.push(role.into());
153		self
154	}
155
156	/// Add multiple roles.
157	pub fn with_roles<S: Into<String>>(mut self, roles: impl IntoIterator<Item = S>) -> Self {
158		for role in roles {
159			self.roles.push(role.into());
160		}
161		self
162	}
163
164	/// Add a custom attribute.
165	pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
166		self.attributes.insert(key.into(), value.into());
167		self
168	}
169
170	/// Check if the user has a specific permission.
171	///
172	/// Also checks for wildcard (*) permission.
173	pub fn has_permission(&self, permission: &str) -> bool {
174		self.permissions.iter().any(|p| p == permission || p == "*")
175	}
176
177	/// Check if the user has a specific role.
178	pub fn has_role(&self, role: &str) -> bool {
179		self.roles.iter().any(|r| r == role)
180	}
181
182	/// Check if the user has any of the given permissions.
183	pub fn has_any_permission(&self, permissions: &[&str]) -> bool {
184		permissions.iter().any(|p| self.has_permission(p))
185	}
186
187	/// Check if the user has all of the given permissions.
188	pub fn has_all_permissions(&self, permissions: &[&str]) -> bool {
189		permissions.iter().all(|p| self.has_permission(p))
190	}
191
192	/// Get a custom attribute.
193	pub fn get_attribute(&self, key: &str) -> Option<&Value> {
194		self.attributes.get(key)
195	}
196}
197
198/// A mock session for testing session-based functionality.
199///
200/// This simulates a server-side session that can store user information
201/// and arbitrary session data.
202///
203/// # Example
204///
205/// ```rust,ignore
206/// // Anonymous session
207/// let anon_session = MockSession::anonymous();
208///
209/// // Authenticated session
210/// let auth_session = MockSession::authenticated(TestUser::authenticated("alice"));
211///
212/// // Session with custom data
213/// let session = MockSession::anonymous()
214///     .with_data("cart_id", serde_json::json!("abc123"))
215///     .with_data("theme", serde_json::json!("dark"));
216/// ```
217#[derive(Debug, Clone, Default, Serialize, Deserialize)]
218pub struct MockSession {
219	/// Session ID.
220	pub id: String,
221	/// The authenticated user, if any.
222	pub user: Option<TestUser>,
223	/// Session data storage.
224	pub data: HashMap<String, Value>,
225	/// CSRF token for the session.
226	pub csrf_token: String,
227	/// Session creation timestamp (Unix epoch seconds).
228	pub created_at: i64,
229	/// Session expiration timestamp (Unix epoch seconds).
230	pub expires_at: Option<i64>,
231	/// Whether the session has been invalidated.
232	pub invalidated: bool,
233}
234
235impl MockSession {
236	/// Create a new anonymous session.
237	pub fn anonymous() -> Self {
238		Self {
239			id: Uuid::now_v7().to_string(),
240			user: None,
241			data: HashMap::new(),
242			csrf_token: generate_csrf_token(),
243			created_at: chrono::Utc::now().timestamp(),
244			expires_at: None,
245			invalidated: false,
246		}
247	}
248
249	/// Create an authenticated session with the given user.
250	pub fn authenticated(user: TestUser) -> Self {
251		Self {
252			id: Uuid::now_v7().to_string(),
253			user: Some(user),
254			data: HashMap::new(),
255			csrf_token: generate_csrf_token(),
256			created_at: chrono::Utc::now().timestamp(),
257			expires_at: None,
258			invalidated: false,
259		}
260	}
261
262	/// Create an authenticated session from a [`SessionIdentity`](crate::auth::SessionIdentity).
263	///
264	/// Bridges the shared identity type to the server_fn test context.
265	/// Creates a stub `TestUser` internally to maintain `is_authenticated()` compatibility.
266	pub fn from_identity(identity: &crate::auth::SessionIdentity) -> Self {
267		let stub_user = TestUser {
268			id: uuid::Uuid::parse_str(&identity.user_id).unwrap_or(uuid::Uuid::nil()),
269			username: identity.user_id.clone(),
270			email: String::new(),
271			permissions: Vec::new(),
272			roles: Vec::new(),
273			is_authenticated: true,
274			attributes: HashMap::new(),
275		};
276		let mut session = Self::authenticated(stub_user);
277		session
278			.data
279			.insert("user_id".into(), serde_json::json!(identity.user_id));
280		session
281			.data
282			.insert("is_staff".into(), serde_json::json!(identity.is_staff));
283		session.data.insert(
284			"is_superuser".into(),
285			serde_json::json!(identity.is_superuser),
286		);
287		session
288	}
289
290	/// Set a custom session ID.
291	pub fn with_id(mut self, id: impl Into<String>) -> Self {
292		self.id = id.into();
293		self
294	}
295
296	/// Add session data.
297	pub fn with_data(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
298		self.data.insert(key.into(), value.into());
299		self
300	}
301
302	/// Set the CSRF token.
303	pub fn with_csrf_token(mut self, token: impl Into<String>) -> Self {
304		self.csrf_token = token.into();
305		self
306	}
307
308	/// Set the session expiration.
309	pub fn with_expiration(mut self, expires_at: i64) -> Self {
310		self.expires_at = Some(expires_at);
311		self
312	}
313
314	/// Set session to expire after the given duration in seconds.
315	pub fn expires_in(mut self, seconds: i64) -> Self {
316		self.expires_at = Some(chrono::Utc::now().timestamp() + seconds);
317		self
318	}
319
320	/// Check if the session is authenticated.
321	pub fn is_authenticated(&self) -> bool {
322		self.user.is_some() && !self.invalidated
323	}
324
325	/// Check if the session has expired.
326	pub fn is_expired(&self) -> bool {
327		if let Some(expires_at) = self.expires_at {
328			chrono::Utc::now().timestamp() > expires_at
329		} else {
330			false
331		}
332	}
333
334	/// Check if the session is valid (not invalidated and not expired).
335	pub fn is_valid(&self) -> bool {
336		!self.invalidated && !self.is_expired()
337	}
338
339	/// Get session data by key.
340	pub fn get<T: for<'de> Deserialize<'de>>(&self, key: &str) -> Option<T> {
341		self.data
342			.get(key)
343			.and_then(|v| serde_json::from_value(v.clone()).ok())
344	}
345
346	/// Get raw session data by key.
347	pub fn get_raw(&self, key: &str) -> Option<&Value> {
348		self.data.get(key)
349	}
350
351	/// Set session data.
352	pub fn set(&mut self, key: impl Into<String>, value: impl Into<Value>) {
353		self.data.insert(key.into(), value.into());
354	}
355
356	/// Remove session data by key.
357	pub fn remove(&mut self, key: &str) -> Option<Value> {
358		self.data.remove(key)
359	}
360
361	/// Clear all session data (but keep user).
362	pub fn clear_data(&mut self) {
363		self.data.clear();
364	}
365
366	/// Invalidate the session.
367	pub fn invalidate(&mut self) {
368		self.invalidated = true;
369	}
370
371	/// Get the user ID if authenticated.
372	pub fn user_id(&self) -> Option<Uuid> {
373		self.user.as_ref().map(|u| u.id)
374	}
375
376	/// Regenerate the session ID (for security after login).
377	pub fn regenerate_id(&mut self) {
378		self.id = Uuid::now_v7().to_string();
379	}
380
381	/// Regenerate the CSRF token.
382	pub fn regenerate_csrf(&mut self) {
383		self.csrf_token = generate_csrf_token();
384	}
385
386	/// Verify a CSRF token.
387	pub fn verify_csrf(&self, token: &str) -> bool {
388		!token.is_empty() && self.csrf_token == token
389	}
390}
391
392/// Token claims for JWT testing.
393///
394/// This represents the claims typically found in a JWT token,
395/// useful for testing JWT-based authentication.
396#[derive(Debug, Clone, Serialize, Deserialize)]
397pub struct TestTokenClaims {
398	/// Subject (user ID).
399	pub sub: String,
400	/// Issued at timestamp.
401	pub iat: i64,
402	/// Expiration timestamp.
403	pub exp: i64,
404	/// Issuer.
405	pub iss: Option<String>,
406	/// Audience.
407	pub aud: Option<String>,
408	/// Custom claims.
409	#[serde(flatten)]
410	pub custom: HashMap<String, Value>,
411}
412
413impl TestTokenClaims {
414	/// Create new token claims for a user.
415	pub fn for_user(user: &TestUser) -> Self {
416		let now = chrono::Utc::now().timestamp();
417		Self {
418			sub: user.id.to_string(),
419			iat: now,
420			exp: now + 3600, // 1 hour default
421			iss: None,
422			aud: None,
423			custom: HashMap::new(),
424		}
425	}
426
427	/// Set expiration duration in seconds from now.
428	pub fn expires_in(mut self, seconds: i64) -> Self {
429		self.exp = chrono::Utc::now().timestamp() + seconds;
430		self
431	}
432
433	/// Set the issuer.
434	pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
435		self.iss = Some(issuer.into());
436		self
437	}
438
439	/// Set the audience.
440	pub fn with_audience(mut self, audience: impl Into<String>) -> Self {
441		self.aud = Some(audience.into());
442		self
443	}
444
445	/// Add a custom claim.
446	pub fn with_claim(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
447		self.custom.insert(key.into(), value.into());
448		self
449	}
450
451	/// Check if the token has expired.
452	pub fn is_expired(&self) -> bool {
453		chrono::Utc::now().timestamp() > self.exp
454	}
455}
456
457/// Generate a random CSRF token.
458fn generate_csrf_token() -> String {
459	// Use UUID for simplicity in tests
460	Uuid::now_v7().to_string().replace('-', "")
461}
462
463/// Test helper for permission assertions.
464pub mod assert_permissions {
465	use super::*;
466
467	/// Assert that the user has the given permission.
468	pub fn has_permission(user: &TestUser, permission: &str) {
469		assert!(
470			user.has_permission(permission),
471			"Expected user '{}' to have permission '{}', but they don't.\nActual permissions: {:?}",
472			user.username,
473			permission,
474			user.permissions
475		);
476	}
477
478	/// Assert that the user does not have the given permission.
479	pub fn lacks_permission(user: &TestUser, permission: &str) {
480		assert!(
481			!user.has_permission(permission),
482			"Expected user '{}' to NOT have permission '{}', but they do.\nActual permissions: {:?}",
483			user.username,
484			permission,
485			user.permissions
486		);
487	}
488
489	/// Assert that the user has the given role.
490	pub fn has_role(user: &TestUser, role: &str) {
491		assert!(
492			user.has_role(role),
493			"Expected user '{}' to have role '{}', but they don't.\nActual roles: {:?}",
494			user.username,
495			role,
496			user.roles
497		);
498	}
499
500	/// Assert that the user does not have the given role.
501	pub fn lacks_role(user: &TestUser, role: &str) {
502		assert!(
503			!user.has_role(role),
504			"Expected user '{}' to NOT have role '{}', but they do.\nActual roles: {:?}",
505			user.username,
506			role,
507			user.roles
508		);
509	}
510
511	/// Assert that the session is authenticated.
512	pub fn is_authenticated(session: &MockSession) {
513		assert!(
514			session.is_authenticated(),
515			"Expected session to be authenticated, but it's not."
516		);
517	}
518
519	/// Assert that the session is anonymous.
520	pub fn is_anonymous(session: &MockSession) {
521		assert!(
522			!session.is_authenticated(),
523			"Expected session to be anonymous, but it's authenticated."
524		);
525	}
526}
527
528#[cfg(test)]
529mod tests {
530	use super::*;
531
532	#[test]
533	fn test_user_anonymous() {
534		let user = TestUser::anonymous();
535		assert!(!user.is_authenticated);
536		assert!(user.permissions.is_empty());
537		assert!(user.roles.is_empty());
538	}
539
540	#[test]
541	fn test_user_authenticated() {
542		let user = TestUser::authenticated("alice");
543		assert!(user.is_authenticated);
544		assert_eq!(user.username, "alice");
545		assert!(user.email.contains("alice"));
546	}
547
548	#[test]
549	fn test_user_admin() {
550		let admin = TestUser::admin();
551		assert!(admin.is_authenticated);
552		assert!(admin.has_permission("admin"));
553		assert!(admin.has_permission("anything")); // Wildcard
554		assert!(admin.has_role("admin"));
555	}
556
557	#[test]
558	fn test_user_permissions() {
559		let user = TestUser::authenticated("bob")
560			.with_permission("read")
561			.with_permission("write");
562
563		assert!(user.has_permission("read"));
564		assert!(user.has_permission("write"));
565		assert!(!user.has_permission("admin"));
566	}
567
568	#[test]
569	fn test_session_anonymous() {
570		let session = MockSession::anonymous();
571		assert!(!session.is_authenticated());
572		assert!(session.is_valid());
573	}
574
575	#[test]
576	fn test_session_authenticated() {
577		let user = TestUser::authenticated("alice");
578		let session = MockSession::authenticated(user);
579
580		assert!(session.is_authenticated());
581		assert!(session.user_id().is_some());
582	}
583
584	#[test]
585	fn test_session_data() {
586		let mut session = MockSession::anonymous();
587		session.set("key", serde_json::json!("value"));
588
589		let value: Option<String> = session.get("key");
590		assert_eq!(value, Some("value".to_string()));
591	}
592
593	#[test]
594	fn test_session_csrf() {
595		let session = MockSession::anonymous().with_csrf_token("test-token");
596
597		assert!(session.verify_csrf("test-token"));
598		assert!(!session.verify_csrf("wrong-token"));
599		assert!(!session.verify_csrf(""));
600	}
601
602	#[test]
603	fn test_session_expiration() {
604		let expired = MockSession::anonymous().expires_in(-100);
605		assert!(expired.is_expired());
606		assert!(!expired.is_valid());
607
608		let valid = MockSession::anonymous().expires_in(3600);
609		assert!(!valid.is_expired());
610		assert!(valid.is_valid());
611	}
612
613	#[test]
614	fn test_token_claims() {
615		let user = TestUser::authenticated("alice");
616		let claims = TestTokenClaims::for_user(&user)
617			.expires_in(3600)
618			.with_issuer("test-issuer")
619			.with_claim("role", "user");
620
621		assert_eq!(claims.sub, user.id.to_string());
622		assert!(!claims.is_expired());
623		assert_eq!(claims.iss, Some("test-issuer".to_string()));
624	}
625}