1pub struct CliProgressBar {}
9
10impl CliProgressBar {
11 pub fn new(msg: &str) -> Self {
12 println!("[ProgressBar] Starting: {}", msg);
14 Self {}
15 }
16 pub fn set_progress(&self, percent: u64) {
17 println!("[ProgressBar] Progress: {}%", percent);
19 }
20 pub fn finish(&self) {
21 println!("[ProgressBar] Finished");
23 }
24}
25
26pub fn format_cli_output(msg: &str) -> String {
27 format!("\x1b[1;34m[auth-framework]\x1b[0m {}", msg)
29}
30#[cfg(feature = "cli")]
31use crate::AppConfig;
32#[cfg(feature = "cli")]
33use crate::auth_operations::UserListQuery;
34#[cfg(feature = "cli")]
35use crate::migrations::MigrationCli;
36#[cfg(feature = "cli")]
37use crate::permissions::{Permission, Role};
38#[cfg(feature = "cli")]
39use clap::{Parser, Subcommand};
40#[cfg(feature = "cli")]
41use std::{io, process};
42
43#[cfg(feature = "cli")]
44#[derive(Parser)]
45#[command(name = "auth-framework")]
46#[command(about = "Auth Framework CLI - Manage authentication and authorization")]
47pub struct Cli {
48 #[command(subcommand)]
49 pub command: Option<Commands>,
50
51 #[arg(short, long, default_value = "auth.toml")]
52 pub config: String,
53
54 #[arg(long)]
55 pub verbose: bool,
56
57 #[arg(short, long)]
58 pub dry_run: bool,
59}
60
61#[cfg(feature = "cli")]
62#[derive(Subcommand)]
63pub enum Commands {
64 Db {
66 #[command(subcommand)]
67 command: DbCommands,
68 },
69 User {
71 #[command(subcommand)]
72 command: UserCommands,
73 },
74 Role {
76 #[command(subcommand)]
77 command: RoleCommands,
78 },
79 System {
81 #[command(subcommand)]
82 command: SystemCommands,
83 },
84 Security {
86 #[command(subcommand)]
87 command: SecurityCommands,
88 },
89}
90
91#[cfg(feature = "cli")]
92#[derive(Subcommand)]
93pub enum DbCommands {
94 Migrate,
96 Status,
98 Reset {
100 #[arg(long)]
101 confirm: bool,
102 },
103 CreateMigration { name: String },
105}
106
107#[cfg(feature = "cli")]
108#[derive(Subcommand)]
109pub enum UserCommands {
110 List {
112 #[arg(short, long)]
113 limit: Option<usize>,
114 #[arg(short, long)]
115 offset: Option<usize>,
116 #[arg(long)]
117 active_only: bool,
118 },
119 Create {
121 email: String,
122 #[arg(short, long)]
123 username: Option<String>,
124 #[arg(short, long)]
125 password: Option<String>,
126 #[arg(long)]
127 admin: bool,
128 },
129 Update {
131 user_id: String,
132 #[arg(short, long)]
133 email: Option<String>,
134 #[arg(short, long)]
135 active: Option<bool>,
136 },
137 Delete {
139 user_id: String,
140 #[arg(long)]
141 confirm: bool,
142 },
143 ResetPassword {
145 user_id: String,
146 #[arg(short, long)]
147 password: Option<String>,
148 },
149 Show { user_id: String },
151}
152
153#[cfg(feature = "cli")]
154#[derive(Subcommand)]
155pub enum RoleCommands {
156 List,
158 Create {
160 name: String,
161 #[arg(short, long)]
162 description: Option<String>,
163 },
164 Assign { user_id: String, role_name: String },
166 Remove { user_id: String, role_name: String },
168 Permissions { role_name: String },
170 AddPermission {
172 role_name: String,
173 permission: String,
174 },
175}
176
177#[cfg(feature = "cli")]
178#[derive(Subcommand)]
179pub enum SystemCommands {
180 Status,
182 Health,
184 Config {
186 #[arg(short, long)]
187 output: Option<String>,
188 },
189 Backup { output_path: String },
191 Restore {
193 backup_path: String,
194 #[arg(long)]
195 confirm: bool,
196 },
197}
198
199#[cfg(feature = "cli")]
200#[derive(Subcommand)]
201pub enum SecurityCommands {
202 Audit {
204 #[arg(short, long)]
205 days: Option<u32>,
206 },
207 Sessions {
209 #[arg(short, long)]
210 user_id: Option<String>,
211 },
212 TerminateSession {
214 session_id: String,
215 #[arg(long)]
216 reason: Option<String>,
217 },
218 LockUser {
220 user_id: String,
221 #[arg(short, long)]
222 reason: Option<String>,
223 },
224 UnlockUser { user_id: String },
226}
227
228#[cfg(feature = "cli")]
229pub struct CliHandler {
230 config: AppConfig,
231 dry_run: bool,
232 }
234
235#[cfg(feature = "cli")]
236impl CliHandler {
237 pub async fn new(config: AppConfig) -> Result<Self, Box<dyn std::error::Error>> {
238 Ok(Self {
240 config,
241 dry_run: false,
242 })
243 }
244
245 async fn framework(&self) -> Result<crate::AuthFramework, Box<dyn std::error::Error>> {
246 Ok(self.config.build_auth_framework().await?)
247 }
248
249 fn prompt_password(prompt: &str) -> Result<String, Box<dyn std::error::Error>> {
250 let password = rpassword::prompt_password(prompt)?;
251 if password.is_empty() {
252 return Err(
253 io::Error::new(io::ErrorKind::InvalidInput, "Password cannot be empty").into(),
254 );
255 }
256 Ok(password)
257 }
258
259 fn default_username_from_email(email: &str) -> String {
260 email
261 .split('@')
262 .next()
263 .filter(|value| !value.is_empty())
264 .unwrap_or("user")
265 .to_string()
266 }
267
268 fn print_user_summary(user: &crate::auth::UserInfo) {
269 let roles = if user.roles.is_empty() {
270 "-".to_string()
271 } else {
272 user.roles.join(", ")
273 };
274 let email = user.email.as_deref().unwrap_or("-");
275 println!(
276 "{}\t{}\t{}\tactive={}\troles={}",
277 user.id, user.username, email, user.active, roles
278 );
279 }
280
281 fn print_role_permissions(role: &Role) {
282 let mut permissions: Vec<String> =
283 role.permissions.iter().map(ToString::to_string).collect();
284 permissions.sort();
285
286 println!("Role: {}", role.name);
287 if let Some(description) = &role.description {
288 println!("Description: {}", description);
289 }
290 println!("Active: {}", role.active);
291 println!("Permissions:");
292 if permissions.is_empty() {
293 println!(" (none)");
294 } else {
295 for permission in permissions {
296 println!(" {}", permission);
297 }
298 }
299 }
300
301 pub async fn handle_command(&mut self, cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
302 self.dry_run = cli.dry_run;
303 match cli.command {
304 Some(Commands::Db { command }) => self.handle_db_command(command).await?,
305 Some(Commands::User { command }) => self.handle_user_command(command).await?,
306 Some(Commands::Role { command }) => self.handle_role_command(command).await?,
307 Some(Commands::System { command }) => self.handle_system_command(command).await?,
308 Some(Commands::Security { command }) => self.handle_security_command(command).await?,
309 None => {
310 eprintln!("No command provided. Use --help for usage.");
311 }
312 }
313 Ok(())
314 }
315
316 async fn handle_db_command(
317 &mut self,
318 command: DbCommands,
319 ) -> Result<(), Box<dyn std::error::Error>> {
320 match command {
321 DbCommands::Migrate => {
322 println!("Running database migrations...");
323 MigrationCli::run(&self.config.database.url, "migrate").await?;
324 }
325 DbCommands::Status => {
326 MigrationCli::run(&self.config.database.url, "status").await?;
327 }
328 DbCommands::Reset { confirm } => {
329 if !confirm {
330 eprintln!("ERROR: Database reset requires --confirm flag");
331 eprintln!("WARNING: This will destroy all data!");
332 process::exit(1);
333 }
334 let framework = self.framework().await?;
335 let report = framework.maintenance().reset(self.dry_run).await?;
336 if report.dry_run {
337 println!(
338 "Dry run: reset would delete {} users, {} roles, {} tokens, {} sessions, and {} KV entries.",
339 report.users_deleted,
340 report.roles_seen,
341 report.tokens_deleted,
342 report.sessions_deleted,
343 report.kv_entries_deleted
344 );
345 } else {
346 println!(
347 "Reset completed: deleted {} users, {} tokens, {} sessions, and {} KV entries.",
348 report.users_deleted,
349 report.tokens_deleted,
350 report.sessions_deleted,
351 report.kv_entries_deleted
352 );
353 }
354 }
355 DbCommands::CreateMigration { name } => {
356 let report = crate::maintenance::create_migration_file(&self.config, &name).await?;
357 println!(
358 "Created {} migration template: {}",
359 report.backend,
360 report.path.display()
361 );
362 }
363 }
364 Ok(())
365 }
366
367 async fn handle_user_command(
368 &mut self,
369 command: UserCommands,
370 ) -> Result<(), Box<dyn std::error::Error>> {
371 match command {
372 UserCommands::List {
373 limit,
374 offset,
375 active_only,
376 } => {
377 let framework = self.framework().await?;
378 let mut query = UserListQuery::new();
379 if let Some(l) = limit {
380 query = query.limit(l);
381 }
382 if let Some(o) = offset {
383 query = query.offset(o);
384 }
385 if active_only {
386 query = query.active_only();
387 }
388 let users = framework.users().list_with_query(query).await?;
389 if users.is_empty() {
390 println!("No users found.");
391 } else {
392 for user in users {
393 Self::print_user_summary(&user);
394 }
395 }
396 }
397 UserCommands::Create {
398 email,
399 username,
400 password,
401 admin,
402 } => {
403 let framework = self.framework().await?;
404 let username =
405 username.unwrap_or_else(|| Self::default_username_from_email(&email));
406 let password = match password {
407 Some(password) => password,
408 None => Self::prompt_password("Password: ")?,
409 };
410 let user_id = framework
411 .users()
412 .register(&username, &email, &password)
413 .await?;
414 if admin {
415 framework
416 .authorization()
417 .assign_role(&user_id, "admin")
418 .await?;
419 }
420 println!("Created user '{}' with ID {}", username, user_id);
421 }
422 UserCommands::Show { user_id } => {
423 let framework = self.framework().await?;
424 let user = framework.users().get(&user_id).await?;
425 Self::print_user_summary(&user);
426 }
427 UserCommands::Update {
428 user_id,
429 email,
430 active,
431 } => {
432 if email.is_none() && active.is_none() {
433 return Err(io::Error::new(
434 io::ErrorKind::InvalidInput,
435 "Provide --email and/or --active when updating a user",
436 )
437 .into());
438 }
439
440 let framework = self.framework().await?;
441 if let Some(email) = email {
442 framework.users().update_email(&user_id, &email).await?;
443 }
444 if let Some(active) = active {
445 framework
446 .users()
447 .set_status(&user_id, active.into())
448 .await?;
449 }
450 println!("Updated user {}", user_id);
451 }
452 UserCommands::Delete { user_id, confirm } => {
453 if !confirm {
454 eprintln!("ERROR: User deletion requires --confirm flag");
455 eprintln!("WARNING: This will permanently delete the user!");
456 process::exit(1);
457 }
458 let framework = self.framework().await?;
459 framework.users().delete_by_id(&user_id).await?;
460 println!("Deleted user {}", user_id);
461 }
462 UserCommands::ResetPassword { user_id, password } => {
463 let framework = self.framework().await?;
464 let password = match password {
465 Some(password) => password,
466 None => Self::prompt_password("New password: ")?,
467 };
468 framework
469 .users()
470 .update_password_by_id(&user_id, &password)
471 .await?;
472 println!("Password updated for user {}", user_id);
473 }
474 }
475
476 Ok(())
477 }
478
479 async fn handle_role_command(
480 &mut self,
481 command: RoleCommands,
482 ) -> Result<(), Box<dyn std::error::Error>> {
483 match command {
484 RoleCommands::List => {
485 let framework = self.framework().await?;
486 let mut roles = framework.authorization().list_roles().await;
487 roles.sort_by(|left, right| left.name.cmp(&right.name));
488 if roles.is_empty() {
489 println!("No roles found.");
490 } else {
491 for role in roles {
492 println!(
493 "{}\tactive={}\tpermissions={}",
494 role.name,
495 role.active,
496 role.permissions.len()
497 );
498 }
499 }
500 }
501 RoleCommands::AddPermission {
502 role_name,
503 permission,
504 } => {
505 let framework = self.framework().await?;
506 let permission = Permission::parse(&permission)?;
507 framework
508 .authorization()
509 .add_role_permission(&role_name, permission)
510 .await?;
511 println!("Added permission to role {}", role_name);
512 }
513 RoleCommands::Create { name, description } => {
514 let framework = self.framework().await?;
515 let role = match description {
516 Some(description) => Role::new(&name).with_description(description),
517 None => Role::new(&name),
518 };
519 framework.authorization().create_role(role).await?;
520 println!("Created role {}", name);
521 }
522 RoleCommands::Assign { user_id, role_name } => {
523 let framework = self.framework().await?;
524 framework
525 .authorization()
526 .assign_role(&user_id, &role_name)
527 .await?;
528 println!("Assigned role {} to {}", role_name, user_id);
529 }
530 RoleCommands::Remove { user_id, role_name } => {
531 let framework = self.framework().await?;
532 framework
533 .authorization()
534 .remove_role(&user_id, &role_name)
535 .await?;
536 println!("Removed role {} from {}", role_name, user_id);
537 }
538 RoleCommands::Permissions { role_name } => {
539 let framework = self.framework().await?;
540 let role = framework.authorization().role(&role_name).await?;
541 Self::print_role_permissions(&role);
542 }
543 }
544
545 Ok(())
546 }
547
548 async fn handle_system_command(
549 &mut self,
550 command: SystemCommands,
551 ) -> Result<(), Box<dyn std::error::Error>> {
552 match command {
553 SystemCommands::Status => {
554 let framework = self.framework().await?;
555 let stats = framework.monitoring().stats().await?;
556 let audit = framework.audit().security_stats().await?;
557 println!(
558 "Registered methods: {}",
559 stats.registered_methods.join(", ")
560 );
561 println!("Active sessions: {}", stats.active_sessions);
562 println!("Active MFA challenges: {}", stats.active_mfa_challenges);
563 println!("Authentication attempts: {}", stats.auth_attempts);
564 println!("Security score: {:.2}", audit.security_score());
565 }
566 SystemCommands::Health => {
567 let framework = self.framework().await?;
568 let health = framework.monitoring().health_check().await?;
569 for (component, result) in health {
570 println!(
571 "{}\t{:?}\t{}\t{}ms",
572 component, result.status, result.message, result.response_time
573 );
574 }
575 }
576 SystemCommands::Config { output } => {
577 let template = include_str!("../config/auth.toml.template");
578 if let Some(path) = output {
579 std::fs::write(&path, template)?;
580 println!("Configuration template written to: {}", path);
581 } else {
582 println!("{}", template);
583 }
584 }
585 SystemCommands::Backup { output_path } => {
586 let framework = self.framework().await?;
587 let report = framework
588 .maintenance()
589 .backup_to_file(&output_path, self.dry_run)
590 .await?;
591 if report.dry_run {
592 println!(
593 "Dry run: backup would write {} users, {} roles, {} tokens, {} sessions, and {} KV entries to {}.",
594 report.manifest.user_count,
595 report.manifest.role_count,
596 report.manifest.token_count,
597 report.manifest.session_count,
598 report.manifest.kv_entry_count,
599 report.output_path.display()
600 );
601 } else {
602 println!(
603 "Backup written to {} (users={}, roles={}, tokens={}, sessions={}, kv={}).",
604 report.output_path.display(),
605 report.manifest.user_count,
606 report.manifest.role_count,
607 report.manifest.token_count,
608 report.manifest.session_count,
609 report.manifest.kv_entry_count
610 );
611 }
612 }
613 SystemCommands::Restore {
614 backup_path,
615 confirm,
616 } => {
617 if !confirm {
618 eprintln!("ERROR: Database restore requires --confirm flag");
619 eprintln!("WARNING: This will overwrite existing data!");
620 process::exit(1);
621 }
622 let framework = self.framework().await?;
623 let report = framework
624 .maintenance()
625 .restore_from_file(&backup_path, self.dry_run)
626 .await?;
627 if report.dry_run {
628 println!(
629 "Dry run: restore would apply snapshot from {} (users={}, roles={}, tokens={}, sessions={}, kv={}).",
630 report.input_path.display(),
631 report.manifest.user_count,
632 report.manifest.role_count,
633 report.manifest.token_count,
634 report.manifest.session_count,
635 report.manifest.kv_entry_count
636 );
637 } else {
638 println!(
639 "Restore completed from {} (users={}, roles={}, tokens={}, sessions={}, kv={}).",
640 report.input_path.display(),
641 report.manifest.user_count,
642 report.manifest.role_count,
643 report.manifest.token_count,
644 report.manifest.session_count,
645 report.manifest.kv_entry_count
646 );
647 }
648 }
649 }
650 Ok(())
651 }
652
653 async fn handle_security_command(
654 &mut self,
655 command: SecurityCommands,
656 ) -> Result<(), Box<dyn std::error::Error>> {
657 match command {
658 SecurityCommands::Audit { days } => {
659 let framework = self.framework().await?;
660 let stats = framework.audit().security_stats().await?;
661 let logs = framework
662 .audit()
663 .permission_logs(None, None, None, Some(20))
664 .await?;
665 println!(
666 "Security audit summary for the last {} day(s):",
667 days.unwrap_or(1)
668 );
669 println!(" active_sessions={}", stats.active_sessions);
670 println!(" failed_logins_24h={}", stats.failed_logins_24h);
671 println!(" successful_logins_24h={}", stats.successful_logins_24h);
672 println!(" security_alerts_24h={}", stats.security_alerts_24h);
673 if logs.is_empty() {
674 println!("Recent permission audit logs: none");
675 } else {
676 println!("Recent permission audit logs:");
677 for log in logs {
678 println!(" {}", log);
679 }
680 }
681 }
682 SecurityCommands::Sessions { user_id } => {
683 let framework = self.framework().await?;
684 if let Some(user_id) = user_id {
685 let sessions = framework.sessions().list_for_user(&user_id).await?;
686 if sessions.is_empty() {
687 println!("No sessions found for user {}", user_id);
688 } else {
689 for session in sessions {
690 println!(
691 "{}\tuser={}\texpires={}\tip={}",
692 session.session_id,
693 session.user_id,
694 session.expires_at,
695 session.ip_address.as_deref().unwrap_or("-")
696 );
697 }
698 }
699 } else {
700 let users = framework.users().list_with_query(UserListQuery::new()).await?;
701 let mut found_any = false;
702 for user in users {
703 let sessions = framework.sessions().list_for_user(&user.id).await?;
704 for session in sessions {
705 found_any = true;
706 println!(
707 "{}\tuser={}\texpires={}\tip={}",
708 session.session_id,
709 session.user_id,
710 session.expires_at,
711 session.ip_address.as_deref().unwrap_or("-")
712 );
713 }
714 }
715 if !found_any {
716 println!("No active sessions found.");
717 }
718 }
719 }
720 SecurityCommands::LockUser { user_id, reason } => {
721 let framework = self.framework().await?;
722 framework
723 .users()
724 .set_status(&user_id, crate::auth_operations::UserStatus::Inactive)
725 .await?;
726 if let Some(reason) = reason {
727 println!("Locked user {}: {}", user_id, reason);
728 } else {
729 println!("Locked user {}", user_id);
730 }
731 }
732 SecurityCommands::TerminateSession { session_id, reason } => {
733 let framework = self.framework().await?;
734 framework.sessions().delete(&session_id).await?;
735 if let Some(reason) = reason {
736 println!("Terminated session {}: {}", session_id, reason);
737 } else {
738 println!("Terminated session {}", session_id);
739 }
740 }
741 SecurityCommands::UnlockUser { user_id } => {
742 let framework = self.framework().await?;
743 framework
744 .users()
745 .set_status(&user_id, crate::auth_operations::UserStatus::Active)
746 .await?;
747 println!("Unlocked user {}", user_id);
748 }
749 }
750
751 Ok(())
752 }
753}
754
755#[cfg(feature = "cli")]
756pub async fn run_cli() -> Result<(), Box<dyn std::error::Error>> {
758 let cli = Cli::parse();
759
760 let config = AppConfig::from_env()?;
762
763 let mut handler = CliHandler::new(config).await?;
765 handler.handle_command(cli).await?;
766
767 Ok(())
768}
769
770#[cfg(all(test, feature = "cli"))]
772mod tests {
773 use super::{
774 Cli, CliHandler, CliProgressBar, Commands, DbCommands, SystemCommands, format_cli_output,
775 };
776 use crate::auth_operations::UserListQuery;
777 use crate::config::app_config::AppConfig;
778 use crate::methods::{AuthMethodEnum, JwtMethod};
779 use crate::permissions::Role;
780 use tempfile::tempdir;
781
782 #[test]
783 fn test_progress_bar() {
784 let pb = CliProgressBar::new("Test");
785 pb.set_progress(50);
786 pb.finish();
787 }
788 #[test]
789 fn test_terminal_formatting() {
790 let msg = format_cli_output("Hello");
791 assert!(msg.contains("[auth-framework]"));
792 }
793
794 #[cfg(all(feature = "cli", feature = "sqlite-storage"))]
795 #[tokio::test]
796 async fn maintenance_cli_smoke_test_roundtrip() {
797 let temp_dir = tempdir().unwrap();
798 let db_path = temp_dir.path().join("maintenance-smoke.db");
799 let snapshot_path = temp_dir.path().join("snapshot.json");
800
801 let database_url = format!(
802 "sqlite://{}?mode=rwc",
803 db_path.to_string_lossy().replace('\\', "/")
804 );
805
806 let mut config = AppConfig::default();
807 config.database.url = database_url;
808
809 let mut seed_framework = config.build_auth_framework().await.unwrap();
810 seed_framework.register_method("jwt", AuthMethodEnum::Jwt(JwtMethod::new()));
811
812 let user_id = seed_framework
813 .users()
814 .register("cli-smoke", "cli-smoke@example.com", "Password123!")
815 .await
816 .unwrap();
817 seed_framework
818 .authorization()
819 .create_role(Role::new("operator"))
820 .await
821 .unwrap();
822 seed_framework
823 .authorization()
824 .assign_role(&user_id, "operator")
825 .await
826 .unwrap();
827 seed_framework
828 .tokens()
829 .create(&user_id, &["read"], "jwt", None)
830 .await
831 .unwrap();
832 seed_framework
833 .sessions()
834 .create(
835 &user_id,
836 std::time::Duration::from_secs(600),
837 Some("127.0.0.1".to_string()),
838 Some("cli-smoke".to_string()),
839 )
840 .await
841 .unwrap();
842 seed_framework
843 .storage()
844 .store_kv("smoke:key", b"present", None)
845 .await
846 .unwrap();
847 drop(seed_framework);
848
849 let mut handler = CliHandler::new(config.clone()).await.unwrap();
850 handler
851 .handle_command(Cli {
852 command: Some(Commands::System {
853 command: SystemCommands::Backup {
854 output_path: snapshot_path.to_string_lossy().to_string(),
855 },
856 }),
857 config: "auth.toml".to_string(),
858 verbose: false,
859 dry_run: false,
860 })
861 .await
862 .unwrap();
863 assert!(snapshot_path.exists());
864
865 handler
866 .handle_command(Cli {
867 command: Some(Commands::Db {
868 command: DbCommands::Reset { confirm: true },
869 }),
870 config: "auth.toml".to_string(),
871 verbose: false,
872 dry_run: false,
873 })
874 .await
875 .unwrap();
876
877 let reset_framework = config.build_auth_framework().await.unwrap();
878 assert!(
879 reset_framework
880 .users()
881 .list_with_query(UserListQuery::new())
882 .await
883 .unwrap()
884 .is_empty()
885 );
886 assert!(
887 reset_framework
888 .storage()
889 .get_kv("smoke:key")
890 .await
891 .unwrap()
892 .is_none()
893 );
894 drop(reset_framework);
895
896 handler
897 .handle_command(Cli {
898 command: Some(Commands::System {
899 command: SystemCommands::Restore {
900 backup_path: snapshot_path.to_string_lossy().to_string(),
901 confirm: true,
902 },
903 }),
904 config: "auth.toml".to_string(),
905 verbose: false,
906 dry_run: false,
907 })
908 .await
909 .unwrap();
910
911 let restored_framework = config.build_auth_framework().await.unwrap();
912 let restored_user = restored_framework.users().get(&user_id).await.unwrap();
913 assert_eq!(restored_user.username, "cli-smoke");
914 assert_eq!(
915 restored_framework
916 .tokens()
917 .list_for_user(&user_id)
918 .await
919 .unwrap()
920 .len(),
921 1
922 );
923 assert_eq!(
924 restored_framework
925 .sessions()
926 .list_for_user(&user_id)
927 .await
928 .unwrap()
929 .len(),
930 1
931 );
932 assert!(
933 restored_framework
934 .authorization()
935 .has_role(&user_id, "operator")
936 .await
937 .unwrap()
938 );
939 assert_eq!(
940 restored_framework
941 .storage()
942 .get_kv("smoke:key")
943 .await
944 .unwrap()
945 .unwrap(),
946 b"present"
947 );
948 drop(restored_framework);
949
950 let original_dir = std::env::current_dir().unwrap();
951 std::env::set_current_dir(temp_dir.path()).unwrap();
952 let migration_result = handler
953 .handle_command(Cli {
954 command: Some(Commands::Db {
955 command: DbCommands::CreateMigration {
956 name: "smoke test migration".to_string(),
957 },
958 }),
959 config: "auth.toml".to_string(),
960 verbose: false,
961 dry_run: false,
962 })
963 .await;
964 std::env::set_current_dir(original_dir).unwrap();
965 migration_result.unwrap();
966
967 let migration_dir = temp_dir.path().join("migrations").join("sqlite");
968 let entries = std::fs::read_dir(&migration_dir)
969 .unwrap()
970 .collect::<Result<Vec<_>, _>>()
971 .unwrap();
972 assert_eq!(entries.len(), 1);
973 }
974}