reinhardt_testkit/server_fn/
auth.rs1#![cfg(native)]
25
26use std::collections::HashMap;
27
28use serde::{Deserialize, Serialize};
29use serde_json::Value;
30use uuid::Uuid;
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct TestUser {
57 pub id: Uuid,
59 pub username: String,
61 pub email: String,
63 pub permissions: Vec<String>,
65 pub roles: Vec<String>,
67 pub is_authenticated: bool,
69 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 pub fn anonymous() -> Self {
90 Self::default()
91 }
92
93 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 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(), ],
115 roles: vec!["admin".to_string(), "superuser".to_string()],
116 is_authenticated: true,
117 attributes: HashMap::new(),
118 }
119 }
120
121 pub fn with_id(mut self, id: Uuid) -> Self {
123 self.id = id;
124 self
125 }
126
127 pub fn with_email(mut self, email: impl Into<String>) -> Self {
129 self.email = email.into();
130 self
131 }
132
133 pub fn with_permission(mut self, permission: impl Into<String>) -> Self {
135 self.permissions.push(permission.into());
136 self
137 }
138
139 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 pub fn with_role(mut self, role: impl Into<String>) -> Self {
152 self.roles.push(role.into());
153 self
154 }
155
156 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 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 pub fn has_permission(&self, permission: &str) -> bool {
174 self.permissions.iter().any(|p| p == permission || p == "*")
175 }
176
177 pub fn has_role(&self, role: &str) -> bool {
179 self.roles.iter().any(|r| r == role)
180 }
181
182 pub fn has_any_permission(&self, permissions: &[&str]) -> bool {
184 permissions.iter().any(|p| self.has_permission(p))
185 }
186
187 pub fn has_all_permissions(&self, permissions: &[&str]) -> bool {
189 permissions.iter().all(|p| self.has_permission(p))
190 }
191
192 pub fn get_attribute(&self, key: &str) -> Option<&Value> {
194 self.attributes.get(key)
195 }
196}
197
198#[derive(Debug, Clone, Default, Serialize, Deserialize)]
218pub struct MockSession {
219 pub id: String,
221 pub user: Option<TestUser>,
223 pub data: HashMap<String, Value>,
225 pub csrf_token: String,
227 pub created_at: i64,
229 pub expires_at: Option<i64>,
231 pub invalidated: bool,
233}
234
235impl MockSession {
236 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 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 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 pub fn with_id(mut self, id: impl Into<String>) -> Self {
292 self.id = id.into();
293 self
294 }
295
296 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 pub fn with_csrf_token(mut self, token: impl Into<String>) -> Self {
304 self.csrf_token = token.into();
305 self
306 }
307
308 pub fn with_expiration(mut self, expires_at: i64) -> Self {
310 self.expires_at = Some(expires_at);
311 self
312 }
313
314 pub fn expires_in(mut self, seconds: i64) -> Self {
316 self.expires_at = Some(chrono::Utc::now().timestamp() + seconds);
317 self
318 }
319
320 pub fn is_authenticated(&self) -> bool {
322 self.user.is_some() && !self.invalidated
323 }
324
325 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 pub fn is_valid(&self) -> bool {
336 !self.invalidated && !self.is_expired()
337 }
338
339 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 pub fn get_raw(&self, key: &str) -> Option<&Value> {
348 self.data.get(key)
349 }
350
351 pub fn set(&mut self, key: impl Into<String>, value: impl Into<Value>) {
353 self.data.insert(key.into(), value.into());
354 }
355
356 pub fn remove(&mut self, key: &str) -> Option<Value> {
358 self.data.remove(key)
359 }
360
361 pub fn clear_data(&mut self) {
363 self.data.clear();
364 }
365
366 pub fn invalidate(&mut self) {
368 self.invalidated = true;
369 }
370
371 pub fn user_id(&self) -> Option<Uuid> {
373 self.user.as_ref().map(|u| u.id)
374 }
375
376 pub fn regenerate_id(&mut self) {
378 self.id = Uuid::now_v7().to_string();
379 }
380
381 pub fn regenerate_csrf(&mut self) {
383 self.csrf_token = generate_csrf_token();
384 }
385
386 pub fn verify_csrf(&self, token: &str) -> bool {
388 !token.is_empty() && self.csrf_token == token
389 }
390}
391
392#[derive(Debug, Clone, Serialize, Deserialize)]
397pub struct TestTokenClaims {
398 pub sub: String,
400 pub iat: i64,
402 pub exp: i64,
404 pub iss: Option<String>,
406 pub aud: Option<String>,
408 #[serde(flatten)]
410 pub custom: HashMap<String, Value>,
411}
412
413impl TestTokenClaims {
414 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, iss: None,
422 aud: None,
423 custom: HashMap::new(),
424 }
425 }
426
427 pub fn expires_in(mut self, seconds: i64) -> Self {
429 self.exp = chrono::Utc::now().timestamp() + seconds;
430 self
431 }
432
433 pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
435 self.iss = Some(issuer.into());
436 self
437 }
438
439 pub fn with_audience(mut self, audience: impl Into<String>) -> Self {
441 self.aud = Some(audience.into());
442 self
443 }
444
445 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 pub fn is_expired(&self) -> bool {
453 chrono::Utc::now().timestamp() > self.exp
454 }
455}
456
457fn generate_csrf_token() -> String {
459 Uuid::now_v7().to_string().replace('-', "")
461}
462
463pub mod assert_permissions {
465 use super::*;
466
467 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 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 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 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 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 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")); 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}