1use axum::{
7 extract::Request,
8 http::{HeaderMap, StatusCode},
9 middleware::Next,
10 response::Response,
11};
12use mockforge_collab::models::UserRole;
13use mockforge_collab::permissions::{Permission, RolePermissions};
14use serde::{Deserialize, Serialize};
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct UserContext {
19 pub user_id: String,
21 pub username: String,
23 pub role: UserRole,
25 pub email: Option<String>,
27}
28
29pub struct AdminActionPermissions;
31
32impl AdminActionPermissions {
33 pub fn get_required_permissions(action: &str) -> Vec<Permission> {
36 match action {
37 "update_latency"
39 | "update_faults"
40 | "update_proxy"
41 | "update_traffic_shaping"
42 | "update_validation" => {
43 vec![Permission::ManageSettings]
44 }
45
46 "restart_servers" | "shutdown_servers" => {
48 vec![Permission::ManageSettings]
49 }
50
51 "clear_logs" | "export_logs" => {
53 vec![Permission::ManageSettings]
54 }
55
56 "create_fixture" => {
58 vec![Permission::MockCreate]
59 }
60 "update_fixture" | "rename_fixture" | "move_fixture" => {
61 vec![Permission::MockUpdate]
62 }
63 "delete_fixture" | "delete_fixtures_bulk" => {
64 vec![Permission::MockDelete]
65 }
66
67 "enable_route" | "disable_route" | "create_route" | "update_route" | "delete_route" => {
69 vec![Permission::MockUpdate]
70 }
71
72 "enable_service" | "disable_service" | "update_service_config" => {
74 vec![Permission::ManageSettings]
75 }
76
77 "create_user" | "update_user" | "delete_user" | "change_role" => {
79 vec![Permission::ChangeRoles]
80 }
81
82 "grant_permission" | "revoke_permission" => {
84 vec![Permission::ChangeRoles]
85 }
86
87 "create_api_key" | "delete_api_key" | "rotate_api_key" => {
89 vec![Permission::ManageSettings]
90 }
91
92 "update_security_policy" => {
94 vec![Permission::ManageSettings]
95 }
96
97 "get_dashboard" | "get_logs" | "get_metrics" | "get_routes" | "get_fixtures"
99 | "get_config" => {
100 vec![Permission::WorkspaceRead, Permission::MockRead]
101 }
102
103 "get_audit_logs" | "get_audit_stats" => {
105 vec![Permission::ManageSettings]
106 }
107
108 "modify_scenario_chaos_rules" | "update_scenario_chaos" => {
111 vec![Permission::ScenarioModifyChaosRules]
112 }
113 "modify_scenario_reality_defaults" | "update_scenario_reality" => {
115 vec![Permission::ScenarioModifyRealityDefaults]
116 }
117 "promote_scenario" | "create_scenario_promotion" => {
119 vec![Permission::ScenarioPromote]
120 }
121 "approve_scenario_promotion" | "reject_scenario_promotion" => {
123 vec![Permission::ScenarioApprove]
124 }
125 "modify_scenario_drift_budget" | "update_scenario_drift_budget" => {
127 vec![Permission::ScenarioModifyDriftBudgets]
128 }
129
130 _ => {
132 vec![Permission::ManageSettings]
133 }
134 }
135 }
136}
137
138pub fn extract_user_context(headers: &HeaderMap) -> Option<UserContext> {
143 if let Some(auth_header) = headers.get("authorization") {
145 if let Ok(auth_str) = auth_header.to_str() {
146 if let Some(token) = auth_str.strip_prefix("Bearer ") {
147 if let Some(user) = parse_jwt_token(token) {
148 return Some(user);
149 }
150 }
151 }
152 }
153
154 let user_id = headers.get("x-user-id")?.to_str().ok()?.to_string();
156 let username = headers.get("x-username")?.to_str().ok()?.to_string();
157 let role_str = headers.get("x-user-role")?.to_str().ok()?;
158 let role = parse_role(role_str)?;
159
160 Some(UserContext {
161 user_id,
162 username,
163 role,
164 email: headers.get("x-user-email").and_then(|h| h.to_str().ok()).map(|s| s.to_string()),
165 })
166}
167
168fn parse_jwt_token(token: &str) -> Option<UserContext> {
171 use crate::auth::{claims_to_user_context, validate_token};
172
173 if let Ok(claims) = validate_token(token) {
175 return Some(claims_to_user_context(&claims));
176 }
177
178 if token.starts_with("mock.") {
180 let parts: Vec<&str> = token.split('.').collect();
181 if parts.len() >= 3 {
182 let payload_part = parts[2];
184 let base64_str = payload_part.replace('-', "+").replace('_', "/");
186 let padding = (4 - (base64_str.len() % 4)) % 4;
188 let padded = format!("{}{}", base64_str, "=".repeat(padding));
189
190 use base64::{engine::general_purpose, Engine as _};
192 if let Ok(decoded) = general_purpose::STANDARD.decode(&padded) {
193 if let Ok(payload_str) = String::from_utf8(decoded) {
194 return parse_jwt_payload(&payload_str);
195 }
196 }
197 }
198 }
199
200 None
201}
202
203fn parse_jwt_payload(payload_str: &str) -> Option<UserContext> {
205 if let Ok(payload) = serde_json::from_str::<serde_json::Value>(payload_str) {
206 let user_id = payload.get("sub")?.as_str()?.to_string();
207 let username = payload.get("username")?.as_str()?.to_string();
208 let role_str = payload.get("role")?.as_str()?;
209 let role = parse_role(role_str)?;
210
211 return Some(UserContext {
212 user_id,
213 username,
214 role,
215 email: payload.get("email").and_then(|v| v.as_str()).map(|s| s.to_string()),
216 });
217 }
218 None
219}
220
221fn parse_role(role_str: &str) -> Option<UserRole> {
223 match role_str.to_lowercase().as_str() {
224 "admin" => Some(UserRole::Admin),
225 "editor" => Some(UserRole::Editor),
226 "viewer" => Some(UserRole::Viewer),
227 _ => None,
228 }
229}
230
231pub fn get_default_user_context() -> Option<UserContext> {
234 if std::env::var("MOCKFORGE_ALLOW_UNAUTHENTICATED").is_ok() {
237 Some(UserContext {
238 user_id: "system".to_string(),
239 username: "system".to_string(),
240 role: UserRole::Admin,
241 email: None,
242 })
243 } else {
244 None
245 }
246}
247
248pub async fn rbac_middleware(mut request: Request, next: Next) -> Result<Response, StatusCode> {
250 let path = request.uri().path();
252 let method = request.method().as_str();
253 let headers = request.headers();
254
255 let is_public_route = path == "/"
257 || path.starts_with("/assets/")
258 || path.starts_with("/__mockforge/auth/")
259 || path == "/__mockforge/health"
260 || path.starts_with("/mockforge-")
261 || path == "/manifest.json"
262 || path == "/sw.js"
263 || path == "/api-docs";
264
265 if is_public_route {
266 return Ok(next.run(request).await);
267 }
268
269 let action_name = match (method, path) {
271 (_, p) if p.contains("/config/latency") => "update_latency",
272 (_, p) if p.contains("/config/faults") => "update_faults",
273 (_, p) if p.contains("/config/proxy") => "update_proxy",
274 (_, p) if p.contains("/config/traffic-shaping") => "update_traffic_shaping",
275 ("DELETE", p) if p.contains("/logs") => "clear_logs",
276 ("POST", p) if p.contains("/restart") => "restart_servers",
277 ("DELETE", p) if p.contains("/fixtures") => "delete_fixture",
278 ("POST", p) if p.contains("/fixtures") && p.contains("/rename") => "rename_fixture",
279 ("POST", p) if p.contains("/fixtures") && p.contains("/move") => "move_fixture",
280 ("GET", p) if p.contains("/audit/logs") => "get_audit_logs",
281 ("GET", p) if p.contains("/audit/stats") => "get_audit_stats",
282 ("GET", _) => "read", _ => "unknown",
284 };
285
286 let user_context = extract_user_context(headers).or_else(get_default_user_context);
288
289 let user_context = match user_context {
291 Some(ctx) => ctx,
292 None => {
293 if std::env::var("MOCKFORGE_ALLOW_UNAUTHENTICATED").is_ok() {
296 get_default_user_context().unwrap_or_else(|| UserContext {
298 user_id: "system".to_string(),
299 username: "system".to_string(),
300 role: UserRole::Admin,
301 email: None,
302 })
303 } else {
304 return Err(StatusCode::UNAUTHORIZED);
305 }
306 }
307 };
308
309 let required_permissions = AdminActionPermissions::get_required_permissions(action_name);
311
312 let has_permission = required_permissions
314 .iter()
315 .any(|&perm| RolePermissions::has_permission(user_context.role, perm));
316
317 if !has_permission {
318 tracing::warn!(
320 user_id = %user_context.user_id,
321 username = %user_context.username,
322 role = ?user_context.role,
323 action = %action_name,
324 "Authorization denied: User does not have required permissions"
325 );
326
327 return Err(StatusCode::FORBIDDEN);
328 }
329
330 request.extensions_mut().insert(user_context);
333
334 Ok(next.run(request).await)
335}
336
337pub fn get_user_context_from_request(request: &Request) -> Option<UserContext> {
339 request.extensions().get::<UserContext>().cloned()
340}
341
342pub fn get_user_context_from_state<T>(state: &T) -> Option<UserContext>
344where
345 T: std::any::Any,
346{
347 None
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354 use axum::http::{HeaderValue, Method};
355
356 #[test]
357 fn test_parse_role_valid() {
358 assert_eq!(parse_role("admin"), Some(UserRole::Admin));
359 assert_eq!(parse_role("Admin"), Some(UserRole::Admin));
360 assert_eq!(parse_role("ADMIN"), Some(UserRole::Admin));
361 assert_eq!(parse_role("editor"), Some(UserRole::Editor));
362 assert_eq!(parse_role("viewer"), Some(UserRole::Viewer));
363 }
364
365 #[test]
366 fn test_parse_role_invalid() {
367 assert_eq!(parse_role("invalid"), None);
368 assert_eq!(parse_role(""), None);
369 assert_eq!(parse_role("super_admin"), None);
370 }
371
372 #[test]
373 fn test_user_context_serialization() {
374 let context = UserContext {
375 user_id: "user123".to_string(),
376 username: "testuser".to_string(),
377 role: UserRole::Editor,
378 email: Some("test@example.com".to_string()),
379 };
380
381 let serialized = serde_json::to_string(&context).unwrap();
382 let deserialized: UserContext = serde_json::from_str(&serialized).unwrap();
383
384 assert_eq!(deserialized.user_id, context.user_id);
385 assert_eq!(deserialized.username, context.username);
386 assert_eq!(deserialized.role, context.role);
387 assert_eq!(deserialized.email, context.email);
388 }
389
390 #[test]
391 fn test_extract_user_context_from_headers() {
392 let mut headers = HeaderMap::new();
393 headers.insert("x-user-id", HeaderValue::from_static("user123"));
394 headers.insert("x-username", HeaderValue::from_static("testuser"));
395 headers.insert("x-user-role", HeaderValue::from_static("admin"));
396 headers.insert("x-user-email", HeaderValue::from_static("test@example.com"));
397
398 let context = extract_user_context(&headers).unwrap();
399 assert_eq!(context.user_id, "user123");
400 assert_eq!(context.username, "testuser");
401 assert_eq!(context.role, UserRole::Admin);
402 assert_eq!(context.email, Some("test@example.com".to_string()));
403 }
404
405 #[test]
406 fn test_extract_user_context_missing_headers() {
407 let headers = HeaderMap::new();
408 let context = extract_user_context(&headers);
409 assert!(context.is_none());
410 }
411
412 #[test]
413 fn test_extract_user_context_partial_headers() {
414 let mut headers = HeaderMap::new();
415 headers.insert("x-user-id", HeaderValue::from_static("user123"));
416 let context = extract_user_context(&headers);
419 assert!(context.is_none());
420 }
421
422 #[test]
423 fn test_extract_user_context_without_email() {
424 let mut headers = HeaderMap::new();
425 headers.insert("x-user-id", HeaderValue::from_static("user123"));
426 headers.insert("x-username", HeaderValue::from_static("testuser"));
427 headers.insert("x-user-role", HeaderValue::from_static("viewer"));
428
429 let context = extract_user_context(&headers).unwrap();
430 assert_eq!(context.user_id, "user123");
431 assert_eq!(context.username, "testuser");
432 assert_eq!(context.role, UserRole::Viewer);
433 assert_eq!(context.email, None);
434 }
435
436 #[test]
437 fn test_parse_jwt_payload() {
438 let payload_json = r#"{
439 "sub": "user456",
440 "username": "jwtuser",
441 "role": "editor",
442 "email": "jwt@example.com"
443 }"#;
444
445 let context = parse_jwt_payload(payload_json).unwrap();
446 assert_eq!(context.user_id, "user456");
447 assert_eq!(context.username, "jwtuser");
448 assert_eq!(context.role, UserRole::Editor);
449 assert_eq!(context.email, Some("jwt@example.com".to_string()));
450 }
451
452 #[test]
453 fn test_parse_jwt_payload_without_email() {
454 let payload_json = r#"{
455 "sub": "user456",
456 "username": "jwtuser",
457 "role": "viewer"
458 }"#;
459
460 let context = parse_jwt_payload(payload_json).unwrap();
461 assert_eq!(context.email, None);
462 }
463
464 #[test]
465 fn test_parse_jwt_payload_invalid_json() {
466 let payload_json = "invalid json";
467 let context = parse_jwt_payload(payload_json);
468 assert!(context.is_none());
469 }
470
471 #[test]
472 fn test_parse_jwt_payload_missing_fields() {
473 let payload_json = r#"{"sub": "user456"}"#;
474 let context = parse_jwt_payload(payload_json);
475 assert!(context.is_none());
476 }
477
478 #[test]
479 fn test_parse_jwt_payload_invalid_role() {
480 let payload_json = r#"{
481 "sub": "user456",
482 "username": "jwtuser",
483 "role": "invalid_role"
484 }"#;
485
486 let context = parse_jwt_payload(payload_json);
487 assert!(context.is_none());
488 }
489
490 #[test]
491 fn test_admin_action_permissions_config_changes() {
492 let perms = AdminActionPermissions::get_required_permissions("update_latency");
493 assert_eq!(perms, vec![Permission::ManageSettings]);
494
495 let perms = AdminActionPermissions::get_required_permissions("update_faults");
496 assert_eq!(perms, vec![Permission::ManageSettings]);
497
498 let perms = AdminActionPermissions::get_required_permissions("update_proxy");
499 assert_eq!(perms, vec![Permission::ManageSettings]);
500 }
501
502 #[test]
503 fn test_admin_action_permissions_fixture_management() {
504 let perms = AdminActionPermissions::get_required_permissions("create_fixture");
505 assert_eq!(perms, vec![Permission::MockCreate]);
506
507 let perms = AdminActionPermissions::get_required_permissions("update_fixture");
508 assert_eq!(perms, vec![Permission::MockUpdate]);
509
510 let perms = AdminActionPermissions::get_required_permissions("delete_fixture");
511 assert_eq!(perms, vec![Permission::MockDelete]);
512 }
513
514 #[test]
515 fn test_admin_action_permissions_user_management() {
516 let perms = AdminActionPermissions::get_required_permissions("create_user");
517 assert_eq!(perms, vec![Permission::ChangeRoles]);
518
519 let perms = AdminActionPermissions::get_required_permissions("change_role");
520 assert_eq!(perms, vec![Permission::ChangeRoles]);
521 }
522
523 #[test]
524 fn test_admin_action_permissions_read_operations() {
525 let perms = AdminActionPermissions::get_required_permissions("get_dashboard");
526 assert_eq!(perms, vec![Permission::WorkspaceRead, Permission::MockRead]);
527
528 let perms = AdminActionPermissions::get_required_permissions("get_logs");
529 assert_eq!(perms, vec![Permission::WorkspaceRead, Permission::MockRead]);
530 }
531
532 #[test]
533 fn test_admin_action_permissions_scenario_operations() {
534 let perms = AdminActionPermissions::get_required_permissions("modify_scenario_chaos_rules");
535 assert_eq!(perms, vec![Permission::ScenarioModifyChaosRules]);
536
537 let perms =
538 AdminActionPermissions::get_required_permissions("modify_scenario_reality_defaults");
539 assert_eq!(perms, vec![Permission::ScenarioModifyRealityDefaults]);
540
541 let perms = AdminActionPermissions::get_required_permissions("promote_scenario");
542 assert_eq!(perms, vec![Permission::ScenarioPromote]);
543
544 let perms = AdminActionPermissions::get_required_permissions("approve_scenario_promotion");
545 assert_eq!(perms, vec![Permission::ScenarioApprove]);
546 }
547
548 #[test]
549 fn test_admin_action_permissions_unknown_action() {
550 let perms = AdminActionPermissions::get_required_permissions("unknown_action");
551 assert_eq!(perms, vec![Permission::ManageSettings]);
552 }
553
554 #[test]
555 fn test_get_default_user_context_without_env_var() {
556 std::env::remove_var("MOCKFORGE_ALLOW_UNAUTHENTICATED");
557 let context = get_default_user_context();
558 assert!(context.is_none());
559 }
560
561 #[test]
562 fn test_get_default_user_context_with_env_var() {
563 std::env::set_var("MOCKFORGE_ALLOW_UNAUTHENTICATED", "1");
564 let context = get_default_user_context();
565 assert!(context.is_some());
566
567 let context = context.unwrap();
568 assert_eq!(context.user_id, "system");
569 assert_eq!(context.username, "system");
570 assert_eq!(context.role, UserRole::Admin);
571
572 std::env::remove_var("MOCKFORGE_ALLOW_UNAUTHENTICATED");
573 }
574
575 #[test]
576 fn test_all_permission_actions_covered() {
577 let actions = vec![
579 "update_latency",
580 "update_faults",
581 "restart_servers",
582 "create_fixture",
583 "update_fixture",
584 "delete_fixture",
585 "enable_route",
586 "create_user",
587 "grant_permission",
588 "create_api_key",
589 "get_dashboard",
590 "get_audit_logs",
591 "modify_scenario_chaos_rules",
592 "promote_scenario",
593 "approve_scenario_promotion",
594 ];
595
596 for action in actions {
597 let perms = AdminActionPermissions::get_required_permissions(action);
598 assert!(!perms.is_empty(), "Action {} should have permissions", action);
599 }
600 }
601
602 #[test]
603 fn test_role_permissions_admin_has_all() {
604 assert!(RolePermissions::has_permission(UserRole::Admin, Permission::ManageSettings));
606 assert!(RolePermissions::has_permission(UserRole::Admin, Permission::MockCreate));
607 assert!(RolePermissions::has_permission(UserRole::Admin, Permission::MockUpdate));
608 assert!(RolePermissions::has_permission(UserRole::Admin, Permission::MockDelete));
609 assert!(RolePermissions::has_permission(UserRole::Admin, Permission::WorkspaceRead));
610 assert!(RolePermissions::has_permission(UserRole::Admin, Permission::ChangeRoles));
611 }
612
613 #[test]
614 fn test_role_permissions_editor_limited() {
615 assert!(!RolePermissions::has_permission(UserRole::Editor, Permission::ManageSettings));
617 assert!(RolePermissions::has_permission(UserRole::Editor, Permission::MockUpdate));
618 assert!(!RolePermissions::has_permission(UserRole::Editor, Permission::ChangeRoles));
619 }
620
621 #[test]
622 fn test_role_permissions_viewer_readonly() {
623 assert!(!RolePermissions::has_permission(UserRole::Viewer, Permission::ManageSettings));
625 assert!(!RolePermissions::has_permission(UserRole::Viewer, Permission::MockCreate));
626 assert!(!RolePermissions::has_permission(UserRole::Viewer, Permission::MockUpdate));
627 assert!(!RolePermissions::has_permission(UserRole::Viewer, Permission::MockDelete));
628 assert!(RolePermissions::has_permission(UserRole::Viewer, Permission::WorkspaceRead));
629 assert!(RolePermissions::has_permission(UserRole::Viewer, Permission::MockRead));
630 }
631
632 #[test]
633 fn test_scenario_permissions() {
634 assert!(RolePermissions::has_permission(
636 UserRole::Admin,
637 Permission::ScenarioModifyChaosRules
638 ));
639 assert!(RolePermissions::has_permission(
640 UserRole::Admin,
641 Permission::ScenarioModifyRealityDefaults
642 ));
643 assert!(RolePermissions::has_permission(UserRole::Admin, Permission::ScenarioPromote));
644 assert!(RolePermissions::has_permission(UserRole::Admin, Permission::ScenarioApprove));
645 }
646
647 #[tokio::test]
648 async fn test_rbac_middleware_public_routes() {
649 use axum::routing::get;
650 use axum::{body::Body, middleware::from_fn, Router};
651 use tower::ServiceExt;
652
653 async fn handler() -> &'static str {
654 "OK"
655 }
656
657 let app = Router::new().route("/", get(handler)).layer(from_fn(rbac_middleware));
658
659 let request = axum::http::Request::builder()
660 .uri("/")
661 .method(Method::GET)
662 .body(Body::empty())
663 .unwrap();
664
665 let response = app.oneshot(request).await.unwrap();
666 assert_eq!(response.status(), StatusCode::OK);
667 }
668
669 #[tokio::test]
670 async fn test_rbac_middleware_health_route() {
671 use axum::routing::get;
672 use axum::{body::Body, middleware::from_fn, Router};
673 use tower::ServiceExt;
674
675 async fn handler() -> &'static str {
676 "OK"
677 }
678
679 let app = Router::new()
680 .route("/__mockforge/health", get(handler))
681 .layer(from_fn(rbac_middleware));
682
683 let request = axum::http::Request::builder()
684 .uri("/__mockforge/health")
685 .method(Method::GET)
686 .body(Body::empty())
687 .unwrap();
688
689 let response = app.oneshot(request).await.unwrap();
690 assert_eq!(response.status(), StatusCode::OK);
691 }
692
693 #[tokio::test]
694 async fn test_rbac_middleware_assets_route() {
695 use axum::routing::get;
696 use axum::{body::Body, middleware::from_fn, Router};
697 use tower::ServiceExt;
698
699 async fn handler() -> &'static str {
700 "OK"
701 }
702
703 let app = Router::new()
704 .route("/assets/style.css", get(handler))
705 .layer(from_fn(rbac_middleware));
706
707 let request = axum::http::Request::builder()
708 .uri("/assets/style.css")
709 .method(Method::GET)
710 .body(Body::empty())
711 .unwrap();
712
713 let response = app.oneshot(request).await.unwrap();
714 assert_eq!(response.status(), StatusCode::OK);
715 }
716
717 #[tokio::test]
718 async fn test_rbac_middleware_with_valid_headers() {
719 use axum::routing::get;
720 use axum::{body::Body, middleware::from_fn, Router};
721 use tower::ServiceExt;
722
723 async fn handler() -> &'static str {
724 "OK"
725 }
726
727 let app = Router::new().route("/api/test", get(handler)).layer(from_fn(rbac_middleware));
728
729 let request = axum::http::Request::builder()
730 .uri("/api/test")
731 .method(Method::GET)
732 .header("x-user-id", "user123")
733 .header("x-username", "testuser")
734 .header("x-user-role", "admin")
735 .body(Body::empty())
736 .unwrap();
737
738 let response = app.oneshot(request).await.unwrap();
739 assert_eq!(response.status(), StatusCode::OK);
740 }
741
742 #[test]
743 fn test_action_name_mapping() {
744 let test_cases = vec![
746 ("/config/latency", "update_latency"),
747 ("/config/faults", "update_faults"),
748 ("/config/proxy", "update_proxy"),
749 ("/logs", "clear_logs"), ("/fixtures/test", "delete_fixture"), ("/audit/logs", "get_audit_logs"), ];
753
754 for (path, expected_action) in test_cases {
757 assert!(!expected_action.is_empty());
758 }
759 }
760
761 #[test]
762 fn test_user_context_clone() {
763 let context = UserContext {
764 user_id: "user123".to_string(),
765 username: "testuser".to_string(),
766 role: UserRole::Editor,
767 email: Some("test@example.com".to_string()),
768 };
769
770 let cloned = context.clone();
771 assert_eq!(cloned.user_id, context.user_id);
772 assert_eq!(cloned.username, context.username);
773 assert_eq!(cloned.role, context.role);
774 assert_eq!(cloned.email, context.email);
775 }
776
777 #[test]
778 fn test_user_context_debug() {
779 let context = UserContext {
780 user_id: "user123".to_string(),
781 username: "testuser".to_string(),
782 role: UserRole::Viewer,
783 email: None,
784 };
785
786 let debug_str = format!("{:?}", context);
787 assert!(debug_str.contains("user123"));
788 assert!(debug_str.contains("testuser"));
789 }
790}