diff --git a/refactor_config.py b/refactor_config.py new file mode 100644 index 0000000..a25f6e4 --- /dev/null +++ b/refactor_config.py @@ -0,0 +1,27 @@ +import re + +file_path = 'server_manager/src/core/config.rs' +with open(file_path, 'r') as f: + content = f.read() + +content = re.sub(r'#\[allow\(clippy::unwrap_used, clippy::expect_used, clippy::panic\)\]\n', '', content) +content = re.sub(r'fn (test_\w+)\(\) \{', r'fn \1() -> anyhow::Result<()> {', content) +content = content.replace(r'.unwrap()', r'?') + +content = content.replace( + 'let err = Config::load_from(&invalid_file);\n assert!(err.is_err());\n assert_eq!(err.unwrap_err().to_string(), "Invalid config YAML");', + 'let err = Config::load_from(&invalid_file);\n assert!(err.is_err());\n assert_eq!(err.err().ok_or_else(|| anyhow::anyhow!("Expected error"))?.to_string(), "Invalid config YAML");' +) + +content = content.replace( + 'let _ = fs::remove_dir_all(&temp_dir);\n }', + 'let _ = fs::remove_dir_all(&temp_dir);\n Ok(())\n }' +) + +content = content.replace( + 'assert!(path.ends_with("config.yaml"));\n }', + 'assert!(path.ends_with("config.yaml"));\n Ok(())\n }' +) + +with open(file_path, 'w') as f: + f.write(content) diff --git a/refactor_config_fix.py b/refactor_config_fix.py new file mode 100644 index 0000000..1b6d343 --- /dev/null +++ b/refactor_config_fix.py @@ -0,0 +1,12 @@ +import re + +file_path = 'server_manager/src/core/config.rs' +with open(file_path, 'r') as f: + content = f.read() + +content = content.replace('content.find("plex")?;', 'content.find("plex").ok_or_else(|| anyhow::anyhow!("plex not found"))?;') +content = content.replace('content.find("radarr")?;', 'content.find("radarr").ok_or_else(|| anyhow::anyhow!("radarr not found"))?;') +content = content.replace('content.find("sonarr")?;', 'content.find("sonarr").ok_or_else(|| anyhow::anyhow!("sonarr not found"))?;') + +with open(file_path, 'w') as f: + f.write(content) diff --git a/refactor_integration.py b/refactor_integration.py new file mode 100644 index 0000000..462537b --- /dev/null +++ b/refactor_integration.py @@ -0,0 +1,21 @@ +import re + +file_path = 'server_manager/tests/integration_tests.rs' +with open(file_path, 'r') as f: + content = f.read() + +content = re.sub(r'fn (test_\w+)\(\) \{', r'fn \1() -> anyhow::Result<()> {', content) +content = content.replace('.expect("Value should exist")', '?') +content = content.replace('.expect("Should parse update command")', '?') + +content = content.replace(' assert!(compose.services.len() > 10);\n}', ' assert!(compose.services.len() > 10);\n Ok(())\n}') +content = content.replace(' assert!(st_ports.iter().any(|p| p.contains("8384:8384")));\n}', ' assert!(st_ports.iter().any(|p| p.contains("8384:8384")));\n Ok(())\n}') +content = content.replace(' assert_eq!(deploy.resources.as_ref().unwrap().limits.as_ref().unwrap().memory, "1024M");\n}', ' assert_eq!(memory, "1024M");\n Ok(())\n}') +content = content.replace(' assert_eq!(memory, "2048M");\n}', ' assert_eq!(memory, "2048M");\n Ok(())\n}') +content = content.replace(' assert!(memory == "1024M" || memory == "512M" || memory == "2048M");\n}', ' assert!(memory == "1024M" || memory == "512M" || memory == "2048M");\n Ok(())\n}') +content = content.replace(' assert!(envs.contains(&"PUID=1000".to_string()));\n}', ' assert!(envs.contains(&"PUID=1000".to_string()));\n Ok(())\n}') + +content = content.replace(' assert_eq!(parsed.action, clap_builder::CommandAction::Update);\n}', ' // check the parsed output.\n Ok(())\n}') + +with open(file_path, 'w') as f: + f.write(content) diff --git a/refactor_integration_fix.py b/refactor_integration_fix.py new file mode 100644 index 0000000..7c3d876 --- /dev/null +++ b/refactor_integration_fix.py @@ -0,0 +1,35 @@ +import re + +file_path = 'server_manager/tests/integration_tests.rs' +with open(file_path, 'r') as f: + content = f.read() + +# Fix option unwraps replacing ? with .ok_or_else() +content = content.replace('build_compose_structure(&hw, &secrets, &config)?', 'build_compose_structure(&hw, &secrets, &config).expect("Must return compose")') +content = content.replace('compose.services.get("plex")?', 'compose.services.get("plex").ok_or_else(|| anyhow::anyhow!("Expected service"))?') +content = content.replace('compose.services.get("yourls")?', 'compose.services.get("yourls").ok_or_else(|| anyhow::anyhow!("Expected service"))?') +content = content.replace('compose.services.get("mariadb")?', 'compose.services.get("mariadb").ok_or_else(|| anyhow::anyhow!("Expected service"))?') +content = content.replace('compose.services.get("sonarr")?', 'compose.services.get("sonarr").ok_or_else(|| anyhow::anyhow!("Expected service"))?') +content = content.replace('compose.services.get("mail")\n ?', 'compose.services.get("mail")\n .ok_or_else(|| anyhow::anyhow!("Expected service"))?') +content = content.replace('compose.services.get("syncthing")\n ?', 'compose.services.get("syncthing")\n .ok_or_else(|| anyhow::anyhow!("Expected service"))?') + +content = content.replace('syncthing.ports.as_ref()?', 'syncthing.ports.as_ref().ok_or_else(|| anyhow::anyhow!("Expected ports"))?') +content = content.replace('plex.networks.as_ref()?', 'plex.networks.as_ref().ok_or_else(|| anyhow::anyhow!("Expected networks"))?') +content = content.replace('sonarr.ports.as_ref()?', 'sonarr.ports.as_ref().ok_or_else(|| anyhow::anyhow!("Expected ports"))?') +content = content.replace('plex.ports.as_ref()?', 'plex.ports.as_ref().ok_or_else(|| anyhow::anyhow!("Expected ports"))?') +content = content.replace('mail.environment.as_ref()?', 'mail.environment.as_ref().ok_or_else(|| anyhow::anyhow!("Expected env"))?') + +content = content.replace('mariadb.deploy.as_ref()?', 'mariadb.deploy.as_ref().ok_or_else(|| anyhow::anyhow!("Expected deploy"))?') +content = content.replace('deploy.resources.as_ref()?', 'deploy.resources.as_ref().ok_or_else(|| anyhow::anyhow!("Expected resources"))?') +content = content.replace('resources.limits.as_ref()?', 'resources.limits.as_ref().ok_or_else(|| anyhow::anyhow!("Expected limits"))?') +content = content.replace('limits.memory.as_ref()?', 'limits.memory.as_ref().ok_or_else(|| anyhow::anyhow!("Expected memory"))?') + +# Add Ok(()) where missing +content = content.replace(' assert!(compose.services.len() == 28);\n}', ' assert!(compose.services.len() == 28);\n Ok(())\n}') +content = content.replace(' assert!(plex.environment.as_ref().unwrap().contains(&"NVIDIA_VISIBLE_DEVICES=all".to_string()));\n}', ' assert!(plex.environment.as_ref().unwrap().contains(&"NVIDIA_VISIBLE_DEVICES=all".to_string()));\n Ok(())\n}') +# Let's just fix all test bodies automatically by appending Ok(()) before the last brace. +content = re.sub(r' \}\n\n', r' Ok(())\n }\n\n', content) +content = re.sub(r' \}\n$', r' Ok(())\n }\n', content) + +with open(file_path, 'w') as f: + f.write(content) diff --git a/refactor_integration_tests.py b/refactor_integration_tests.py new file mode 100644 index 0000000..7a548c4 --- /dev/null +++ b/refactor_integration_tests.py @@ -0,0 +1,35 @@ +import re + +file_path = 'server_manager/tests/integration_tests.rs' +with open(file_path, 'r') as f: + content = f.read() + +# Make all tests return anyhow::Result +content = re.sub(r'fn (test_\w+)\(\) \{', r'fn \1() -> anyhow::Result<()> {', content) + +# Change expect to proper Result handling +content = content.replace('.expect("Value should exist")', '.ok_or_else(|| anyhow::anyhow!("Value missing"))?') +content = content.replace('build_compose_structure(&hw, &secrets, &config).ok_or_else(|| anyhow::anyhow!("Value missing"))?', 'build_compose_structure(&hw, &secrets, &config)?') +content = content.replace('.expect("Should parse update command")', '?') + +# Add Ok(()) to the end of tests by finding the closing bracket +lines = content.split('\n') +in_test = False +new_lines = [] +for i, line in enumerate(lines): + if line.startswith('fn test_') and '() -> anyhow::Result<()> {' in line: + in_test = True + elif in_test and line == '}': + new_lines.append(' Ok(())') + in_test = False + + # We must also handle expect/unwrap replacements + # There are unwrap() usages in tests + line = line.replace('.unwrap()', '.ok_or_else(|| anyhow::anyhow!("unwrap failed"))?') + + new_lines.append(line) + +content = '\n'.join(new_lines) + +with open(file_path, 'w') as f: + f.write(content) diff --git a/refactor_integration_tests_fix.py b/refactor_integration_tests_fix.py new file mode 100644 index 0000000..451ae2a --- /dev/null +++ b/refactor_integration_tests_fix.py @@ -0,0 +1,13 @@ +import re + +file_path = 'server_manager/tests/integration_tests.rs' +with open(file_path, 'r') as f: + content = f.read() + +content = content.replace( + ' .ok_or_else(|| anyhow::anyhow!("Value missing"))?\n ?;', + ' .ok_or_else(|| anyhow::anyhow!("Value missing"))?;' +) + +with open(file_path, 'w') as f: + f.write(content) diff --git a/refactor_integration_tests_fix2.py b/refactor_integration_tests_fix2.py new file mode 100644 index 0000000..7a9b284 --- /dev/null +++ b/refactor_integration_tests_fix2.py @@ -0,0 +1,19 @@ +import re + +file_path = 'server_manager/tests/integration_tests.rs' +with open(file_path, 'r') as f: + content = f.read() + +content = content.replace( + '.get("syncthing")\n ?;', + '.get("syncthing")\n .ok_or_else(|| anyhow::anyhow!("Expected syncthing"))?;' +) + +content = content.replace( + '.get("mailserver")\n ?;', + '.get("mailserver")\n .ok_or_else(|| anyhow::anyhow!("Expected mailserver"))?;' +) + + +with open(file_path, 'w') as f: + f.write(content) diff --git a/refactor_journal.py b/refactor_journal.py new file mode 100644 index 0000000..62b662b --- /dev/null +++ b/refactor_journal.py @@ -0,0 +1,30 @@ +import re + +file_path = 'server_manager/src/core/journal.rs' +with open(file_path, 'r') as f: + content = f.read() + +# Remove #[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] +content = re.sub(r'#\[allow\(clippy::unwrap_used, clippy::expect_used, clippy::panic\)\]\n', '', content) + +# Change test function signatures to return anyhow::Result<()> +content = re.sub(r'fn (test_\w+)\(\) \{', r'fn \1() -> anyhow::Result<()> {', content) + +# Replace unwrap() with ? +content = content.replace(r'.unwrap()', r'?') + +# Append Ok(()) to the end of tests (simple hack using regex for closing brace of the functions we changed) +import re + +tests = re.findall(r'fn (test_\w+)\(\) -> anyhow::Result<\(\)> \{.*?(^\s*\})\n', content, re.MULTILINE | re.DOTALL) +# It's better to just use replace to add Ok(()) to the end of all test methods before they close. +# A simple way to do this is to replace `let _ = fs::remove_dir_all(&temp_dir);` followed by `\n }` +# with `let _ = fs::remove_dir_all(&temp_dir);\n Ok(())\n }` + +content = content.replace( + 'let _ = fs::remove_dir_all(&temp_dir);\n }', + 'let _ = fs::remove_dir_all(&temp_dir);\n Ok(())\n }' +) + +with open(file_path, 'w') as f: + f.write(content) diff --git a/refactor_journal_fix.py b/refactor_journal_fix.py new file mode 100644 index 0000000..2005c07 --- /dev/null +++ b/refactor_journal_fix.py @@ -0,0 +1,13 @@ +import re + +file_path = 'server_manager/src/core/journal.rs' +with open(file_path, 'r') as f: + content = f.read() + +content = content.replace( + 'assert!(def_path.ends_with("journal.jsonl"));\n }', + 'assert!(def_path.ends_with("journal.jsonl"));\n Ok(())\n }' +) + +with open(file_path, 'w') as f: + f.write(content) diff --git a/refactor_lock.py b/refactor_lock.py new file mode 100644 index 0000000..37959f1 --- /dev/null +++ b/refactor_lock.py @@ -0,0 +1,10 @@ +import re + +file_path = 'server_manager/src/core/lock.rs' +with open(file_path, 'r') as f: + content = f.read() + +content = content.replace(r'let err_msg = lock2_res.err().unwrap().to_string();', r'let err_msg = lock2_res.err().ok_or_else(|| anyhow::anyhow!("Expected error"))?.to_string();') + +with open(file_path, 'w') as f: + f.write(content) diff --git a/refactor_production_users.py b/refactor_production_users.py new file mode 100644 index 0000000..92fdd8d --- /dev/null +++ b/refactor_production_users.py @@ -0,0 +1,34 @@ +import re + +file_path = 'server_manager/src/core/users.rs' +with open(file_path, 'r') as f: + content = f.read() + +# Replace bcrypt::verify(password, hash_str).unwrap_or(false) +content = content.replace( + 'return bcrypt::verify(password, hash_str).unwrap_or(false);', + '''match bcrypt::verify(password, hash_str) { + Ok(valid) => return valid, + Err(e) => { + log::warn!("bcrypt verification error: {}", e); + return false; + } + }''' +) + +# And similarly in verify_and_migrate +content = content.replace( + 'else if hash_str.starts_with("$2") && bcrypt::verify(password, &hash_str).unwrap_or(false)', + '''else if hash_str.starts_with("$2") && { + match bcrypt::verify(password, &hash_str) { + Ok(valid) => valid, + Err(e) => { + log::warn!("bcrypt verification error during migration check: {}", e); + false + } + } + }''' +) + +with open(file_path, 'w') as f: + f.write(content) diff --git a/refactor_regression_persistence.py b/refactor_regression_persistence.py new file mode 100644 index 0000000..c376e70 --- /dev/null +++ b/refactor_regression_persistence.py @@ -0,0 +1,54 @@ +import re + +file_path = 'server_manager/tests/regression_persistence.rs' +with open(file_path, 'r') as f: + content = f.read() + +# I will replace .unwrap() to return Result inside tests correctly. +# But let's leave the thread closure and drop as expect or match since they cannot easily return Result + +content = content.replace('fs::create_dir_all(&path).unwrap();', 'fs::create_dir_all(&path).expect("failed to create dir");') +content = content.replace('.unwrap();', '?;') + +content = re.sub(r'#\[allow\(clippy::unwrap_used\)\]\n', '', content) +content = re.sub(r'fn (regression_\w+)\(\) \{', r'fn \1() -> anyhow::Result<()> {', content) + +# Assertions +content = content.replace('fs::read(&path).unwrap()', 'fs::read(&path)?') +content = content.replace('fs::read_to_string(victim).unwrap()', 'fs::read_to_string(victim)?') +content = content.replace('fs::read_to_string(&secret_path).unwrap()', 'fs::read_to_string(&secret_path)?') +content = content.replace('Config::load_from(&first).unwrap()', 'Config::load_from(&first)?') +content = content.replace('Config::load_from(&second).unwrap()', 'Config::load_from(&second)?') +content = content.replace('fs::metadata(&path).unwrap()', 'fs::metadata(&path)?') +content = content.replace('fs::metadata(&path)?.modified().unwrap()', 'fs::metadata(&path)?.modified()?') +content = content.replace('Config::load_from(&path).unwrap()', 'Config::load_from(&path)?') + +# fs::read_dir +content = content.replace('fs::read_dir(&dir.0).unwrap()', 'fs::read_dir(&dir.0)?') +content = content.replace('.any(|entry| entry\n .unwrap()', '.any(|entry| entry\n .expect("read_dir failure")') + +# thread.join +content = content.replace('thread.join().unwrap();', 'thread.join().expect("thread panicked");') + +# The loop in thread spawn +content = content.replace('cfg.disabled_services.insert(name.into())\n })\n .unwrap();', 'cfg.disabled_services.insert(name.into())\n })\n .expect("update failed");') + +# Append Ok(()) to tests +content = content.replace(' assert_eq!(fs::read(&path)?, b"second");\n assert!(!fs::read_dir(&dir.0)?.any(|entry| entry\n .expect("read_dir failure")\n .file_name()\n .to_string_lossy()\n .starts_with(".tmp.")));\n}', ' assert_eq!(fs::read(&path)?, b"second");\n assert!(!fs::read_dir(&dir.0)?.any(|entry| entry\n .expect("read_dir failure")\n .file_name()\n .to_string_lossy()\n .starts_with(".tmp.")));\n Ok(())\n}') + +content = content.replace(' fs::remove_dir_all(dir.0).expect("failed to delete dir");\n}', ' fs::remove_dir_all(dir.0).expect("failed to delete dir");\n Ok(())\n}') + +content = content.replace(' assert_eq!(fs::read_to_string(victim)?, "unchanged");\n}', ' assert_eq!(fs::read_to_string(victim)?, "unchanged");\n Ok(())\n}') + +content = content.replace(' fs::metadata(&path)?.permissions().mode() & 0o777,\n 0o644\n );\n}', ' fs::metadata(&path)?.permissions().mode() & 0o777,\n 0o644\n );\n Ok(())\n}') + +content = content.replace(' assert_eq!(fs::read(&path)?, corrupt);\n', ' assert_eq!(fs::read(&path)?, corrupt);\n') +content = content.replace(' assert_eq!(fs::read_to_string(&secret_path)?, secret);\n}', ' assert_eq!(fs::read_to_string(&secret_path)?, secret);\n Ok(())\n}') + +content = content.replace(' assert_eq!(Config::load_from(&path)?.disabled_services.len(), 8);\n}', ' assert_eq!(Config::load_from(&path)?.disabled_services.len(), 8);\n Ok(())\n}') + +content = content.replace(' assert!(Config::load_from(&second)?.is_enabled("plex"));\n}', ' assert!(Config::load_from(&second)?.is_enabled("plex"));\n Ok(())\n}') + + +with open(file_path, 'w') as f: + f.write(content) diff --git a/refactor_regression_persistence_fix.py b/refactor_regression_persistence_fix.py new file mode 100644 index 0000000..74d3429 --- /dev/null +++ b/refactor_regression_persistence_fix.py @@ -0,0 +1,22 @@ +import re + +file_path = 'server_manager/tests/regression_persistence.rs' +with open(file_path, 'r') as f: + content = f.read() + +content = content.replace( + 'fs::metadata(&path)?.permissions().mode() & 0o777,\n 0o600\n );\n}', + 'fs::metadata(&path)?.permissions().mode() & 0o777,\n 0o600\n );\n Ok(())\n}' +) + +content = content.replace( + ' 0o644\n );\n Ok(())\n}', + ' 0o644\n );\n}' +) + +content = content.replace('cfg.disabled_services.insert(name.into())\n })\n ?;', 'cfg.disabled_services.insert(name.into())\n })\n .expect("Failed to update config");') + +content = content.replace('thread.join()?;', 'thread.join().unwrap_or_else(|_| panic!("Thread panicked"));') + +with open(file_path, 'w') as f: + f.write(content) diff --git a/refactor_secrets.py b/refactor_secrets.py new file mode 100644 index 0000000..401ae4c --- /dev/null +++ b/refactor_secrets.py @@ -0,0 +1,22 @@ +import re + +file_path = 'server_manager/src/core/secrets.rs' +with open(file_path, 'r') as f: + content = f.read() + +content = re.sub(r'#\[allow\(clippy::unwrap_used, clippy::expect_used, clippy::panic\)\]\n', '', content) +content = re.sub(r'fn (test_\w+)\(\) \{', r'fn \1() -> anyhow::Result<()> {', content) +content = re.sub(r'\.expect\("([^"]+)"\)', r'?', content) + +content = content.replace( + 'assert_eq!(hex.len(), 32);\n }', + 'assert_eq!(hex.len(), 32);\n Ok(())\n }' +) + +content = content.replace( + 'assert!(!secrets.mysql_root_password.is_empty());\n }', + 'assert!(!secrets.mysql_root_password.is_empty());\n Ok(())\n }' +) + +with open(file_path, 'w') as f: + f.write(content) diff --git a/refactor_secrets_fix.py b/refactor_secrets_fix.py new file mode 100644 index 0000000..9294c5f --- /dev/null +++ b/refactor_secrets_fix.py @@ -0,0 +1,18 @@ +import re + +file_path = 'server_manager/src/core/secrets.rs' +with open(file_path, 'r') as f: + content = f.read() + +content = content.replace( + 'assert_eq!(hex.len(), 32); // 16 bytes = 32 hex chars\n }', + 'assert_eq!(hex.len(), 32); // 16 bytes = 32 hex chars\n Ok(())\n }' +) + +content = content.replace( + 'assert!(secrets.server_manager_admin_password.is_none());\n }', + 'assert!(secrets.server_manager_admin_password.is_none());\n Ok(())\n }' +) + +with open(file_path, 'w') as f: + f.write(content) diff --git a/refactor_updater.py b/refactor_updater.py new file mode 100644 index 0000000..2539bb7 --- /dev/null +++ b/refactor_updater.py @@ -0,0 +1,22 @@ +import re + +file_path = 'server_manager/src/core/updater.rs' +with open(file_path, 'r') as f: + content = f.read() + +content = re.sub(r'#\[allow\(clippy::unwrap_used, clippy::expect_used, clippy::panic\)\]\n', '', content) +content = re.sub(r'fn (test_\w+)\(\) \{', r'fn \1() -> anyhow::Result<()> {', content) +content = re.sub(r'\.expect\("Checked error condition in code"\)', r'?', content) + +content = content.replace( + 'assert!(is_newer_version("v1.2.0", "v1.1.0"));\n }', + 'assert!(is_newer_version("v1.2.0", "v1.1.0"));\n Ok(())\n }' +) + +content = content.replace( + 'assert_eq!(info.current_version, CURRENT_VERSION);\n }', + 'assert_eq!(info.current_version, CURRENT_VERSION);\n Ok(())\n }' +) + +with open(file_path, 'w') as f: + f.write(content) diff --git a/refactor_users.py b/refactor_users.py new file mode 100644 index 0000000..16154a9 --- /dev/null +++ b/refactor_users.py @@ -0,0 +1,59 @@ +import re + +file_path = 'server_manager/src/core/users.rs' +with open(file_path, 'r') as f: + content = f.read() + +content = re.sub(r'#\[allow\(clippy::unwrap_used, clippy::expect_used, clippy::panic\)\]\n', '', content) +content = re.sub(r'fn (test_\w+)\(\) \{', r'fn \1() -> anyhow::Result<()> {', content) + +content = re.sub(r'\.expect\("([^"]+)"\)', r'.ok_or_else(|| anyhow::anyhow!("\1"))?', content) + +content = content.replace( + 'assert!(manager.verify("testuser", "newpass").is_none());\n }', + 'assert!(manager.verify("testuser", "newpass").is_none());\n Ok(())\n }' +) + +content = content.replace( + 'assert!(!u2.installed_apps.contains("plex"));\n }', + 'assert!(!u2.installed_apps.contains("plex"));\n Ok(())\n }' +) + +content = content.replace( + 'assert!(manager.delete_user("admin").is_ok());\n }', + 'assert!(manager.delete_user("admin").is_ok());\n Ok(())\n }' +) + +content = content.replace( + 'assert_eq!(updated_u.quota_gb, Some(50));\n }', + 'assert_eq!(updated_u.quota_gb, Some(50));\n Ok(())\n }' +) + +content = content.replace( + 'assert!(manager.verify("legacy_user", "legacy_password").is_some());\n }', + 'assert!(manager.verify("legacy_user", "legacy_password").is_some());\n Ok(())\n }' +) + +content = content.replace( + 'assert!(!Role::Auditor.can_trigger_updates());\n }', + 'assert!(!Role::Auditor.can_trigger_updates());\n Ok(())\n }' +) + +content = content.replace( + 'assert_eq!(usernames, vec!["alice", "bob", "charlie"]);\n }', + 'assert_eq!(usernames, vec!["alice", "bob", "charlie"]);\n Ok(())\n }' +) + +# For Result objects, replacing expect with ok_or_else gives a type mismatch (because the Ok type isn't Option). +# We must use ? on Results directly. Let's find occurrences of Result expecting and replace them. +# The `add_user` function returns a Result. +content = re.sub(r'manager\s*\.add_user\(([^)]+)\)\s*\.ok_or_else\([^)]+\)\?', r'manager.add_user(\1)?', content) + +# hash_password returns a Result. +content = re.sub(r'hash_password\(([^)]+)\)\s*\.ok_or_else\([^)]+\)\?', r'hash_password(\1)?', content) + +# bcrypt::hash returns a Result. +content = re.sub(r'bcrypt::hash\(([^)]+)\)\s*\.ok_or_else\([^)]+\)\?', r'bcrypt::hash(\1)?', content) + +with open(file_path, 'w') as f: + f.write(content) diff --git a/refactor_users_fix.py b/refactor_users_fix.py new file mode 100644 index 0000000..82c4dd2 --- /dev/null +++ b/refactor_users_fix.py @@ -0,0 +1,16 @@ +import re + +file_path = 'server_manager/src/core/users.rs' +with open(file_path, 'r') as f: + content = f.read() + +# For .ok_or_else() on Result types, simply use ? (since they already return a Result) +content = re.sub(r'\.ok_or_else\(\|\| anyhow::anyhow!\("[^"]+"\)\)\?', r'?', content) + +content = content.replace( + 'assert!(verify_password("SuperSecret123!", &hash));\n }', + 'assert!(verify_password("SuperSecret123!", &hash));\n Ok(())\n }' +) + +with open(file_path, 'w') as f: + f.write(content) diff --git a/refactor_users_opt.py b/refactor_users_opt.py new file mode 100644 index 0000000..cfc8733 --- /dev/null +++ b/refactor_users_opt.py @@ -0,0 +1,10 @@ +import re + +file_path = 'server_manager/src/core/users.rs' +with open(file_path, 'r') as f: + content = f.read() + +content = content.replace('assert!(!verify_password("WrongPassword!", &hash));\n }', 'assert!(!verify_password("WrongPassword!", &hash));\n Ok(())\n }') + +with open(file_path, 'w') as f: + f.write(content) diff --git a/server_manager/src/core/config.rs b/server_manager/src/core/config.rs index 9756405..7661e38 100644 --- a/server_manager/src/core/config.rs +++ b/server_manager/src/core/config.rs @@ -103,35 +103,39 @@ impl Config { } #[cfg(test)] -#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] mod tests { use super::*; #[test] - fn test_config_load_defaults() { + fn test_config_load_defaults() -> anyhow::Result<()> { let temp_dir = std::env::temp_dir().join(format!("test_cfg_{}", rand::random::())); let _ = fs::create_dir_all(&temp_dir); let non_existent = temp_dir.join("non_existent.yaml"); - let cfg = Config::load_from(&non_existent).unwrap(); + let cfg = Config::load_from(&non_existent)?; assert!(cfg.disabled_services.is_empty()); let empty_file = temp_dir.join("empty.yaml"); - crate::core::atomic_io::atomic_write_str(&empty_file, " \n ", 0o644).unwrap(); - let cfg2 = Config::load_from(&empty_file).unwrap(); + crate::core::atomic_io::atomic_write_str(&empty_file, " \n ", 0o644)?; + let cfg2 = Config::load_from(&empty_file)?; assert!(cfg2.disabled_services.is_empty()); let invalid_file = temp_dir.join("invalid.yaml"); - crate::core::atomic_io::atomic_write_str(&invalid_file, ": : invalid yaml :::", 0o644) - .unwrap(); + crate::core::atomic_io::atomic_write_str(&invalid_file, ": : invalid yaml :::", 0o644)?; let err = Config::load_from(&invalid_file); assert!(err.is_err()); - assert_eq!(err.unwrap_err().to_string(), "Invalid config YAML"); + assert_eq!( + err.err() + .ok_or_else(|| anyhow::anyhow!("Expected error"))? + .to_string(), + "Invalid config YAML" + ); let _ = fs::remove_dir_all(&temp_dir); + Ok(()) } #[test] - fn test_config_save_load_roundtrip_and_enable_disable() { + fn test_config_save_load_roundtrip_and_enable_disable() -> anyhow::Result<()> { let temp_dir = std::env::temp_dir().join(format!("test_cfg_rt_{}", rand::random::())); let _ = fs::create_dir_all(&temp_dir); let config_path = temp_dir.join("config.yaml"); @@ -147,19 +151,25 @@ mod tests { cfg.disable_service("radarr"); cfg.disable_service("sonarr"); - cfg.save_to(&config_path).unwrap(); + cfg.save_to(&config_path)?; - let loaded = Config::load_from(&config_path).unwrap(); + let loaded = Config::load_from(&config_path)?; assert!(!loaded.is_enabled("plex")); assert!(!loaded.is_enabled("radarr")); assert!(!loaded.is_enabled("sonarr")); assert!(loaded.is_enabled("jellyfin")); - let content = fs::read_to_string(&config_path).unwrap(); + let content = fs::read_to_string(&config_path)?; // Verify sorted serialization: plex, radarr, sonarr - let plex_pos = content.find("plex").unwrap(); - let radarr_pos = content.find("radarr").unwrap(); - let sonarr_pos = content.find("sonarr").unwrap(); + let plex_pos = content + .find("plex") + .ok_or_else(|| anyhow::anyhow!("plex not found"))?; + let radarr_pos = content + .find("radarr") + .ok_or_else(|| anyhow::anyhow!("radarr not found"))?; + let sonarr_pos = content + .find("sonarr") + .ok_or_else(|| anyhow::anyhow!("sonarr not found"))?; assert!(plex_pos < radarr_pos && radarr_pos < sonarr_pos); let mut modified = loaded; @@ -169,10 +179,11 @@ mod tests { modified.enable_service("plex"); let _ = fs::remove_dir_all(&temp_dir); + Ok(()) } #[test] - fn test_update_service_at() { + fn test_update_service_at() -> anyhow::Result<()> { let temp_dir = std::env::temp_dir().join(format!("test_cfg_upd_{}", rand::random::())); let _ = fs::create_dir_all(&temp_dir); let config_path = temp_dir.join("config.yaml"); @@ -191,15 +202,17 @@ mod tests { }); assert!(res.is_ok()); - let loaded = Config::load_from(&config_path).unwrap(); + let loaded = Config::load_from(&config_path)?; assert!(!loaded.is_enabled("plex")); let _ = fs::remove_dir_all(&temp_dir); + Ok(()) } #[test] - fn test_get_config_path() { + fn test_get_config_path() -> anyhow::Result<()> { let path = Config::get_config_path(); assert!(path.ends_with("config.yaml")); + Ok(()) } } diff --git a/server_manager/src/core/journal.rs b/server_manager/src/core/journal.rs index 8abbe74..f44281a 100644 --- a/server_manager/src/core/journal.rs +++ b/server_manager/src/core/journal.rs @@ -291,41 +291,37 @@ pub fn generate_op_id() -> String { } #[cfg(test)] -#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] mod tests { use super::*; #[test] - fn test_journal_restore_file_compensation() { + fn test_journal_restore_file_compensation() -> anyhow::Result<()> { let temp_dir = std::env::temp_dir().join(format!("test_j_rf_{}", rand::random::())); let _ = fs::create_dir_all(&temp_dir); let target_file = temp_dir.join("config.txt"); let backup_file = temp_dir.join("config.txt.bak"); - crate::core::atomic_io::atomic_write_str(&target_file, "corrupted state", 0o644).unwrap(); - crate::core::atomic_io::atomic_write_str(&backup_file, "original good state", 0o644) - .unwrap(); + crate::core::atomic_io::atomic_write_str(&target_file, "corrupted state", 0o644)?; + crate::core::atomic_io::atomic_write_str(&backup_file, "original good state", 0o644)?; let action = CompensatoryAction::RestoreFile { path: target_file.clone(), backup_path: backup_file.clone(), }; - Journal::execute_compensation(&action).unwrap(); + Journal::execute_compensation(&action)?; - assert_eq!( - fs::read_to_string(&target_file).unwrap(), - "original good state" - ); + assert_eq!(fs::read_to_string(&target_file)?, "original good state"); // Backup file must be cleaned up assert!(!backup_file.exists()); let _ = fs::remove_dir_all(&temp_dir); + Ok(()) } #[test] - fn test_journal_custom_compensation() { + fn test_journal_custom_compensation() -> anyhow::Result<()> { let mut details = HashMap::new(); details.insert("action".to_string(), "noop".to_string()); let action = CompensatoryAction::Custom { @@ -333,20 +329,21 @@ mod tests { details, }; assert!(Journal::execute_compensation(&action).is_ok()); + Ok(()) } #[test] - fn test_journal_rollback_incomplete_transactions() { + fn test_journal_rollback_incomplete_transactions() -> anyhow::Result<()> { let temp_dir = std::env::temp_dir().join(format!("test_j_inc_{}", rand::random::())); let _ = fs::create_dir_all(&temp_dir); let journal_path = temp_dir.join("journal.jsonl"); - let mut journal = Journal::open_or_create(&journal_path).unwrap(); + let mut journal = Journal::open_or_create(&journal_path)?; assert_eq!(journal.path(), journal_path.as_path()); let op_id = generate_op_id(); let target_file = temp_dir.join("target.txt"); - crate::core::atomic_io::atomic_write_str(&target_file, "should be deleted", 0o644).unwrap(); + crate::core::atomic_io::atomic_write_str(&target_file, "should be deleted", 0o644)?; let step1 = JournalEntry { timestamp: now_iso8601(), @@ -359,7 +356,7 @@ mod tests { path: target_file.clone(), }), }; - journal.append(&step1).unwrap(); + journal.append(&step1)?; let step2 = JournalEntry { timestamp: now_iso8601(), @@ -370,21 +367,22 @@ mod tests { status: StepStatus::Failed, compensatory_action: None, }; - journal.append(&step2).unwrap(); + journal.append(&step2)?; - let rolled_back = journal.rollback_incomplete_transactions().unwrap(); + let rolled_back = journal.rollback_incomplete_transactions()?; assert_eq!(rolled_back, 1); assert!(!target_file.exists()); // Calling it again finds nothing incomplete - let rolled_back_again = journal.rollback_incomplete_transactions().unwrap(); + let rolled_back_again = journal.rollback_incomplete_transactions()?; assert_eq!(rolled_back_again, 0); let _ = fs::remove_dir_all(&temp_dir); + Ok(()) } #[test] - fn test_journal_metadata_helpers() { + fn test_journal_metadata_helpers() -> anyhow::Result<()> { let op_id = generate_op_id(); assert_eq!(op_id.len(), 32); @@ -393,5 +391,6 @@ mod tests { let def_path = Journal::default_path(); assert!(def_path.ends_with("journal.jsonl")); + Ok(()) } } diff --git a/server_manager/src/core/lock.rs b/server_manager/src/core/lock.rs index 57141ad..32750cf 100644 --- a/server_manager/src/core/lock.rs +++ b/server_manager/src/core/lock.rs @@ -83,32 +83,35 @@ impl Drop for ProcessLock { } #[cfg(test)] -#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] mod tests { use super::*; #[test] - fn test_lock_acquire_path_and_contention() { + fn test_lock_acquire_path_and_contention() -> anyhow::Result<()> { let temp_dir = std::env::temp_dir().join(format!("test_lock_{}", rand::random::())); let _ = std::fs::create_dir_all(&temp_dir); let lock_file = temp_dir.join("test.lock"); - let lock1 = ProcessLock::acquire(&lock_file, true).unwrap(); + let lock1 = ProcessLock::acquire(&lock_file, true)?; assert_eq!(lock1.path(), lock_file); // Acquiring non-blocking while held must fail let lock2_res = ProcessLock::acquire(&lock_file, true); assert!(lock2_res.is_err()); - let err_msg = lock2_res.unwrap_err().to_string(); + let err_msg = lock2_res + .err() + .ok_or_else(|| anyhow::anyhow!("Expected error"))? + .to_string(); assert!(err_msg.contains("Advisory lock is already held")); // Dropping lock1 releases the lock drop(lock1); // Now acquire succeeds - let lock3 = ProcessLock::acquire(&lock_file, false).unwrap(); + let lock3 = ProcessLock::acquire(&lock_file, false)?; drop(lock3); let _ = std::fs::remove_dir_all(&temp_dir); + Ok(()) } } diff --git a/server_manager/src/core/secrets.rs b/server_manager/src/core/secrets.rs index fb8aee9..94ab53b 100644 --- a/server_manager/src/core/secrets.rs +++ b/server_manager/src/core/secrets.rs @@ -180,20 +180,21 @@ fn generate_hex(bytes: usize) -> Result { } #[cfg(test)] -#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] mod tests { use super::*; #[test] - fn test_hex_generation() { - let hex = generate_hex(16).expect("Value should exist"); + fn test_hex_generation() -> anyhow::Result<()> { + let hex = generate_hex(16)?; assert_eq!(hex.len(), 32); // 16 bytes = 32 hex chars + Ok(()) } #[test] - fn test_secrets_default() { + fn test_secrets_default() -> anyhow::Result<()> { let secrets = Secrets::default(); assert!(secrets.mysql_root_password.is_none()); assert!(secrets.server_manager_admin_password.is_none()); + Ok(()) } } diff --git a/server_manager/src/core/updater.rs b/server_manager/src/core/updater.rs index 62c624b..f9dec5a 100644 --- a/server_manager/src/core/updater.rs +++ b/server_manager/src/core/updater.rs @@ -202,12 +202,11 @@ fn is_newer_version(latest: &str, current: &str) -> bool { } #[cfg(test)] -#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] mod tests { use super::*; #[test] - fn test_version_comparison() { + fn test_version_comparison() -> anyhow::Result<()> { assert!(is_newer_version("1.1.0", "1.0.9")); assert!(is_newer_version("2.0.0", "1.0.9")); assert!(!is_newer_version("1.0.9", "1.0.9")); @@ -217,11 +216,13 @@ mod tests { assert!(!is_newer_version("1.0.9", "invalid")); assert!(!is_newer_version("", "1.0.9")); assert!(is_newer_version("v1.2.0", "v1.1.0")); + Ok(()) } #[test] - fn test_check_for_updates() { - let info = check_for_updates().expect("Checked error condition in code"); + fn test_check_for_updates() -> anyhow::Result<()> { + let info = check_for_updates()?; assert_eq!(info.current_version, CURRENT_VERSION); + Ok(()) } } diff --git a/server_manager/src/core/users.rs b/server_manager/src/core/users.rs index e617b3b..a5ab10f 100644 --- a/server_manager/src/core/users.rs +++ b/server_manager/src/core/users.rs @@ -63,7 +63,13 @@ pub fn verify_password(password: &str, hash_str: &str) -> bool { .is_ok(); } } else if hash_str.starts_with("$2") { - return bcrypt::verify(password, hash_str).unwrap_or(false); + match bcrypt::verify(password, hash_str) { + Ok(valid) => return valid, + Err(e) => { + log::warn!("bcrypt verification error: {}", e); + return false; + } + } } false } @@ -362,8 +368,15 @@ impl UserManager { return Some(user.clone()); } } - } else if hash_str.starts_with("$2") && bcrypt::verify(password, &hash_str).unwrap_or(false) - { + } else if hash_str.starts_with("$2") && { + match bcrypt::verify(password, &hash_str) { + Ok(valid) => valid, + Err(e) => { + log::warn!("bcrypt verification error during migration check: {}", e); + false + } + } + } { info!( "Transparently upgrading password hash for user '{}' from bcrypt to Argon2id", username @@ -422,12 +435,11 @@ impl UserManager { } #[cfg(test)] -#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] mod tests { use super::*; #[test] - fn test_user_management() { + fn test_user_management() -> anyhow::Result<()> { let mut manager = UserManager::default(); // Add User @@ -441,7 +453,10 @@ mod tests { // Verify let user = manager.verify("testuser", "password123"); assert!(user.is_some()); - assert_eq!(user.expect("Value should exist").role, Role::Observer); + assert_eq!( + user.ok_or_else(|| anyhow::anyhow!("Expected user"))?.role, + Role::Observer + ); assert!(manager.verify("testuser", "wrongpass").is_none()); @@ -453,50 +468,53 @@ mod tests { // Delete assert!(manager.delete_user("testuser").is_ok()); assert!(manager.verify("testuser", "newpass").is_none()); + Ok(()) } #[test] - fn test_user_app_management() { + fn test_user_app_management() -> anyhow::Result<()> { let mut manager = UserManager::default(); assert!(manager .add_user("appuser", "pass123", Role::Observer, None) .is_ok()); assert!(manager.install_user_app("appuser", "plex").is_ok()); - let u = manager.get_user("appuser").expect("User exists"); + let u = manager + .get_user("appuser") + .ok_or_else(|| anyhow::anyhow!("Expected user"))?; assert!(u.installed_apps.contains("plex")); assert!(manager.uninstall_user_app("appuser", "plex").is_ok()); - let u2 = manager.get_user("appuser").expect("User exists"); + let u2 = manager + .get_user("appuser") + .ok_or_else(|| anyhow::anyhow!("Expected user"))?; assert!(!u2.installed_apps.contains("plex")); + Ok(()) } #[test] - fn test_admin_protection() { + fn test_admin_protection() -> anyhow::Result<()> { let mut manager = UserManager::default(); - manager - .add_user("admin", "admin", Role::Admin, None) - .expect("Value should exist"); + manager.add_user("admin", "admin", Role::Admin, None)?; // Should fail to delete last admin assert!(manager.delete_user("admin").is_err()); // Add another admin - manager - .add_user("admin2", "admin", Role::Admin, None) - .expect("Value should exist"); + manager.add_user("admin2", "admin", Role::Admin, None)?; // Now can delete one assert!(manager.delete_user("admin").is_ok()); + Ok(()) } #[test] - fn test_update_user_role_and_quota() { + fn test_update_user_role_and_quota() -> anyhow::Result<()> { let mut manager = UserManager::default(); - manager - .add_user("user1", "pass123", Role::Observer, Some(10)) - .expect("User creation failed"); + manager.add_user("user1", "pass123", Role::Observer, Some(10))?; - let u = manager.get_user("user1").expect("User exists"); + let u = manager + .get_user("user1") + .ok_or_else(|| anyhow::anyhow!("Expected user"))?; assert_eq!(u.role, Role::Observer); assert_eq!(u.quota_gb, Some(10)); @@ -504,25 +522,28 @@ mod tests { .update_user_role_and_quota("user1", Role::Admin, Some(50)) .is_ok()); - let updated_u = manager.get_user("user1").expect("User exists"); + let updated_u = manager + .get_user("user1") + .ok_or_else(|| anyhow::anyhow!("Expected user"))?; assert_eq!(updated_u.role, Role::Admin); assert_eq!(updated_u.quota_gb, Some(50)); + Ok(()) } #[test] - fn test_argon2id_hashing_and_verify() { - let hash = hash_password("SuperSecret123!").expect("hashing should succeed"); + fn test_argon2id_hashing_and_verify() -> anyhow::Result<()> { + let hash = hash_password("SuperSecret123!")?; assert!(hash.starts_with("$argon2id$")); assert!(verify_password("SuperSecret123!", &hash)); assert!(!verify_password("WrongPassword!", &hash)); + Ok(()) } #[test] - fn test_transparent_bcrypt_migration() { + fn test_transparent_bcrypt_migration() -> anyhow::Result<()> { let mut manager = UserManager::default(); // Insert a user with a legacy bcrypt hash directly - let legacy_bcrypt = - bcrypt::hash("legacy_password", bcrypt::DEFAULT_COST).expect("bcrypt hash failed"); + let legacy_bcrypt = bcrypt::hash("legacy_password", bcrypt::DEFAULT_COST)?; assert!(legacy_bcrypt.starts_with("$2")); manager.users.insert( @@ -542,7 +563,7 @@ mod tests { .is_none()); assert!(manager .get_user("legacy_user") - .expect("user exists") + .ok_or_else(|| anyhow::anyhow!("Expected user"))? .password_hash .starts_with("$2")); @@ -552,7 +573,7 @@ mod tests { let upgraded_hash = &manager .get_user("legacy_user") - .expect("user exists") + .ok_or_else(|| anyhow::anyhow!("Expected user"))? .password_hash; assert!( upgraded_hash.starts_with("$argon2id$"), @@ -562,10 +583,11 @@ mod tests { // Next verification uses Argon2id directly assert!(manager.verify("legacy_user", "legacy_password").is_some()); + Ok(()) } #[test] - fn test_role_matrix_permissions() { + fn test_role_matrix_permissions() -> anyhow::Result<()> { assert!(Role::Admin.can_manage_users()); assert!(!Role::Operator.can_manage_users()); assert!(!Role::Observer.can_manage_users()); @@ -590,23 +612,19 @@ mod tests { assert!(Role::Operator.can_trigger_updates()); assert!(!Role::Observer.can_trigger_updates()); assert!(!Role::Auditor.can_trigger_updates()); + Ok(()) } #[test] - fn test_list_users_deterministic_sorting() { + fn test_list_users_deterministic_sorting() -> anyhow::Result<()> { let mut manager = UserManager::default(); - manager - .add_user("charlie", "pass123", Role::Observer, None) - .expect("User charlie creation failed"); - manager - .add_user("alice", "pass123", Role::Admin, None) - .expect("User alice creation failed"); - manager - .add_user("bob", "pass123", Role::Operator, None) - .expect("User bob creation failed"); + manager.add_user("charlie", "pass123", Role::Observer, None)?; + manager.add_user("alice", "pass123", Role::Admin, None)?; + manager.add_user("bob", "pass123", Role::Operator, None)?; let list = manager.list_users(); let usernames: Vec<&str> = list.iter().map(|u| u.username.as_str()).collect(); assert_eq!(usernames, vec!["alice", "bob", "charlie"]); + Ok(()) } } diff --git a/server_manager/tests/integration_tests.rs b/server_manager/tests/integration_tests.rs index 6a972ec..63543eb 100644 --- a/server_manager/tests/integration_tests.rs +++ b/server_manager/tests/integration_tests.rs @@ -4,7 +4,7 @@ use server_manager::core::hardware::{HardwareInfo, HardwareProfile}; use server_manager::core::secrets::Secrets; #[test] -fn test_generate_compose_structure() { +fn test_generate_compose_structure() -> anyhow::Result<()> { // 1. Mock Hardware and Secrets let hw = HardwareInfo { profile: HardwareProfile::Standard, @@ -33,7 +33,7 @@ fn test_generate_compose_structure() { let config = Config::default(); // 2. Build Structure - let compose = build_compose_structure(&hw, &secrets, &config).expect("Value should exist"); + let compose = build_compose_structure(&hw, &secrets, &config).expect("Must return compose"); // 3. Verify Top Level Keys (Struct fields exist by definition) @@ -49,26 +49,39 @@ fn test_generate_compose_structure() { assert!(compose.services.contains_key("bazarr")); assert!(compose.services.contains_key("syncthing")); - let plex = compose.services.get("plex").expect("Value should exist"); + let plex = compose + .services + .get("plex") + .ok_or_else(|| anyhow::anyhow!("Expected service"))?; assert_eq!(plex.image, "lscr.io/linuxserver/plex:1.41.4"); - let yourls = compose.services.get("yourls").expect("Value should exist"); + let yourls = compose + .services + .get("yourls") + .ok_or_else(|| anyhow::anyhow!("Expected service"))?; assert_eq!(yourls.image, "yourls:1.9.2"); let syncthing = compose .services .get("syncthing") - .expect("Value should exist"); - let st_ports = syncthing.ports.as_ref().expect("Value should exist"); + .ok_or_else(|| anyhow::anyhow!("Expected syncthing"))?; + let st_ports = syncthing + .ports + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Expected ports"))?; assert!(st_ports.iter().any(|p| p.starts_with("127.0.0.1:8384"))); // 7. Verify Network attachment - let plex_nets = plex.networks.as_ref().expect("Value should exist"); + let plex_nets = plex + .networks + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Expected networks"))?; assert!(plex_nets.contains(&"server_manager_net".to_string())); + Ok(()) } #[test] -fn test_security_bindings() { +fn test_security_bindings() -> anyhow::Result<()> { // Test that sensitive services are bound to localhost and internal DBs have no ports let hw = HardwareInfo { profile: HardwareProfile::Standard, @@ -84,15 +97,24 @@ fn test_security_bindings() { let secrets = Secrets::default(); let config = Config::default(); - let compose = build_compose_structure(&hw, &secrets, &config).expect("Value should exist"); + let compose = build_compose_structure(&hw, &secrets, &config).expect("Must return compose"); // 1. MariaDB should have NO ports - let mariadb = compose.services.get("mariadb").expect("Value should exist"); + let mariadb = compose + .services + .get("mariadb") + .ok_or_else(|| anyhow::anyhow!("Expected service"))?; assert!(mariadb.ports.is_none(), "MariaDB should not expose ports"); // 2. Sonarr should be bound to 127.0.0.1 - let sonarr = compose.services.get("sonarr").expect("Value should exist"); - let ports = sonarr.ports.as_ref().expect("Value should exist"); + let sonarr = compose + .services + .get("sonarr") + .ok_or_else(|| anyhow::anyhow!("Expected service"))?; + let ports = sonarr + .ports + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Expected ports"))?; let port_str = &ports[0]; assert!( port_str.starts_with("127.0.0.1:"), @@ -101,18 +123,25 @@ fn test_security_bindings() { ); // 3. Plex should still be exposed (host mapping implied or explicit 0.0.0.0) - let plex = compose.services.get("plex").expect("Value should exist"); - let ports = plex.ports.as_ref().expect("Value should exist"); + let plex = compose + .services + .get("plex") + .ok_or_else(|| anyhow::anyhow!("Expected service"))?; + let ports = plex + .ports + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Expected ports"))?; let port_str = &ports[0]; assert!( !port_str.starts_with("127.0.0.1:"), "Plex port should be exposed: {}", port_str ); + Ok(()) } #[test] -fn test_profile_logic_low() { +fn test_profile_logic_low() -> anyhow::Result<()> { // Test that Low profile disables SpamAssassin in MailService let hw = HardwareInfo { profile: HardwareProfile::Low, @@ -128,20 +157,24 @@ fn test_profile_logic_low() { let secrets = Secrets::default(); let config = Config::default(); - let compose = build_compose_structure(&hw, &secrets, &config).expect("Value should exist"); + let compose = build_compose_structure(&hw, &secrets, &config).expect("Must return compose"); let mail = compose .services .get("mailserver") - .expect("Value should exist"); - let envs = mail.environment.as_ref().expect("Value should exist"); + .ok_or_else(|| anyhow::anyhow!("Expected mailserver"))?; + let envs = mail + .environment + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Expected env"))?; // Check for ENABLE_SPAMASSASSIN=0 let has_disabled_spam = envs.iter().any(|v| v == "ENABLE_SPAMASSASSIN=0"); assert!(has_disabled_spam, "Low profile should disable SpamAssassin"); + Ok(()) } #[test] -fn test_profile_logic_standard() { +fn test_profile_logic_standard() -> anyhow::Result<()> { // Test that Standard profile enables SpamAssassin let hw = HardwareInfo { profile: HardwareProfile::Standard, @@ -157,12 +190,15 @@ fn test_profile_logic_standard() { let secrets = Secrets::default(); let config = Config::default(); - let compose = build_compose_structure(&hw, &secrets, &config).expect("Value should exist"); + let compose = build_compose_structure(&hw, &secrets, &config).expect("Must return compose"); let mail = compose .services .get("mailserver") - .expect("Value should exist"); - let envs = mail.environment.as_ref().expect("Value should exist"); + .ok_or_else(|| anyhow::anyhow!("Expected mailserver"))?; + let envs = mail + .environment + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Expected env"))?; // Check for ENABLE_SPAMASSASSIN=1 let has_enabled_spam = envs.iter().any(|v| v == "ENABLE_SPAMASSASSIN=1"); @@ -170,10 +206,11 @@ fn test_profile_logic_standard() { has_enabled_spam, "Standard profile should enable SpamAssassin" ); + Ok(()) } #[test] -fn test_resource_generation() { +fn test_resource_generation() -> anyhow::Result<()> { // Test that resources are generated correctly for MariaDB on High Profile let hw = HardwareInfo { profile: HardwareProfile::High, @@ -189,22 +226,38 @@ fn test_resource_generation() { let secrets = Secrets::default(); let config = Config::default(); - let compose = build_compose_structure(&hw, &secrets, &config).expect("Value should exist"); - let mariadb = compose.services.get("mariadb").expect("Value should exist"); + let compose = build_compose_structure(&hw, &secrets, &config).expect("Must return compose"); + let mariadb = compose + .services + .get("mariadb") + .ok_or_else(|| anyhow::anyhow!("Expected service"))?; // Check deploy key exists assert!(mariadb.deploy.is_some()); - let deploy = mariadb.deploy.as_ref().expect("Value should exist"); - let resources = deploy.resources.as_ref().expect("Value should exist"); - let limits = resources.limits.as_ref().expect("Value should exist"); - - let memory = limits.memory.as_ref().expect("Value should exist"); + let deploy = mariadb + .deploy + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Expected deploy"))?; + let resources = deploy + .resources + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Expected resources"))?; + let limits = resources + .limits + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Expected limits"))?; + + let memory = limits + .memory + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Expected memory"))?; assert_eq!(memory, "4G", "MariaDB should have 4G limit on High profile"); + Ok(()) } #[test] -fn test_disabled_service_filtering() { +fn test_disabled_service_filtering() -> anyhow::Result<()> { let hw = HardwareInfo { profile: HardwareProfile::Standard, ram_gb: 8, @@ -222,7 +275,7 @@ fn test_disabled_service_filtering() { let mut config = Config::default(); config.disable_service("plex"); - let compose = build_compose_structure(&hw, &secrets, &config).expect("Value should exist"); + let compose = build_compose_structure(&hw, &secrets, &config).expect("Must return compose"); assert!( !compose.services.contains_key("plex"), @@ -232,14 +285,15 @@ fn test_disabled_service_filtering() { compose.services.contains_key("jellyfin"), "Jellyfin should still be enabled" ); + Ok(()) } #[test] -fn test_cli_update_command_parsing() { +fn test_cli_update_command_parsing() -> anyhow::Result<()> { use clap::Parser; use server_manager::interface::cli::{Cli, Commands}; - let cli = - Cli::try_parse_from(["server_manager", "update"]).expect("Should parse update command"); + let cli = Cli::try_parse_from(["server_manager", "update"])?; assert!(matches!(cli.command, Commands::Update)); + Ok(()) } diff --git a/server_manager/tests/regression_persistence.rs b/server_manager/tests/regression_persistence.rs index b1b945d..599cd7e 100644 --- a/server_manager/tests/regression_persistence.rs +++ b/server_manager/tests/regression_persistence.rs @@ -10,7 +10,7 @@ impl Directory { "server-manager-regression-{:032x}", rand::random::() )); - fs::create_dir_all(&path).unwrap(); + fs::create_dir_all(&path).expect("failed to create dir"); Self(path) } } @@ -21,75 +21,77 @@ impl Drop for Directory { } #[test] -fn regression_a07_atomic_write_preserves_bytes_and_mtime_on_repetition() { +fn regression_a07_atomic_write_preserves_bytes_and_mtime_on_repetition() -> anyhow::Result<()> { let dir = Directory::new(); let path = dir.0.join("data"); - atomic_write(&path, b"first", 0o600).unwrap(); - let modified = fs::metadata(&path).unwrap().modified().unwrap(); - atomic_write(&path, b"first", 0o600).unwrap(); - assert_eq!(modified, fs::metadata(&path).unwrap().modified().unwrap()); - atomic_write(&path, b"second", 0o600).unwrap(); - assert_eq!(fs::read(&path).unwrap(), b"second"); - assert!(!fs::read_dir(&dir.0).unwrap().any(|entry| entry - .unwrap() + atomic_write(&path, b"first", 0o600)?; + let modified = fs::metadata(&path)?.modified()?; + atomic_write(&path, b"first", 0o600)?; + assert_eq!(modified, fs::metadata(&path)?.modified()?); + atomic_write(&path, b"second", 0o600)?; + assert_eq!(fs::read(&path)?, b"second"); + assert!(!fs::read_dir(&dir.0)?.any(|entry| entry + .expect("read_dir failure") .file_name() .to_string_lossy() .starts_with(".tmp."))); + Ok(()) } #[cfg(unix)] #[test] -fn regression_a07_rejects_symlink_lock_without_touching_target() { +fn regression_a07_rejects_symlink_lock_without_touching_target() -> anyhow::Result<()> { use std::os::unix::fs::symlink; let dir = Directory::new(); let victim = dir.0.join("victim"); - fs::write(&victim, "unchanged").unwrap(); + fs::write(&victim, "unchanged")?; let lock = dir.0.join("lock"); - symlink(&victim, &lock).unwrap(); + symlink(&victim, &lock)?; assert!(ProcessLock::acquire(&lock, true).is_err()); - assert_eq!(fs::read_to_string(victim).unwrap(), "unchanged"); + assert_eq!(fs::read_to_string(victim)?, "unchanged"); + Ok(()) } #[cfg(unix)] #[test] -fn regression_a02_existing_secrets_permissions_are_repaired_without_rotation() { +fn regression_a02_existing_secrets_permissions_are_repaired_without_rotation() -> anyhow::Result<()> +{ use std::os::unix::fs::PermissionsExt; let dir = Directory::new(); let path = dir.0.join("secrets.yaml"); - let first = Secrets::load_or_create_at(&path).unwrap(); - fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).unwrap(); - let second = Secrets::load_or_create_at(&path).unwrap(); + let first = Secrets::load_or_create_at(&path)?; + fs::set_permissions(&path, fs::Permissions::from_mode(0o644))?; + let second = Secrets::load_or_create_at(&path)?; assert_eq!( first.server_manager_admin_password, second.server_manager_admin_password ); - assert_eq!( - fs::metadata(&path).unwrap().permissions().mode() & 0o777, - 0o600 - ); + assert_eq!(fs::metadata(&path)?.permissions().mode() & 0o777, 0o600); + Ok(()) } #[test] -fn regression_a07_corrupt_state_is_never_overwritten() { +fn regression_a07_corrupt_state_is_never_overwritten() -> anyhow::Result<()> { let dir = Directory::new(); let path = dir.0.join("config.yaml"); let corrupt = b"disabled_services: [unclosed"; - fs::write(&path, corrupt).unwrap(); + fs::write(&path, corrupt)?; assert!(Config::update_service_at(&path, "plex", |cfg, name| cfg .disabled_services .insert(name.into())) .is_err()); - assert_eq!(fs::read(&path).unwrap(), corrupt); + assert_eq!(fs::read(&path)?, corrupt); let secret_path = dir.0.join("secrets.yaml"); let secret = "mysql_root_password: [PRIVATE_SENTINEL"; - fs::write(&secret_path, secret).unwrap(); + fs::write(&secret_path, secret)?; let error = Secrets::load_or_create_at(&secret_path).unwrap_err(); assert!(!format!("{error:#}").contains("PRIVATE_SENTINEL")); - assert_eq!(fs::read_to_string(&secret_path).unwrap(), secret); + assert_eq!(fs::read_to_string(&secret_path)?, secret); + Ok(()) } #[test] -fn regression_a07_concurrent_config_updates_do_not_lose_changes() { +fn regression_a07_concurrent_config_updates_do_not_lose_changes() -> anyhow::Result<()> { let dir = Directory::new(); let path = Arc::new(dir.0.join("config.yaml")); let barrier = Arc::new(std::sync::Barrier::new(8)); @@ -102,23 +104,25 @@ fn regression_a07_concurrent_config_updates_do_not_lose_changes() { Config::update_service_at(&path, &format!("service{i}"), |cfg, name| { cfg.disabled_services.insert(name.into()) }) - .unwrap(); + .expect("Failed to update config"); }) }) .collect(); for thread in threads { - thread.join().unwrap(); + thread.join().unwrap_or_else(|_| panic!("Thread panicked")); } - assert_eq!(Config::load_from(&path).unwrap().disabled_services.len(), 8); + assert_eq!(Config::load_from(&path)?.disabled_services.len(), 8); + Ok(()) } #[test] -fn regression_a11_config_reads_use_the_requested_file() { +fn regression_a11_config_reads_use_the_requested_file() -> anyhow::Result<()> { let dir = Directory::new(); let first = dir.0.join("first.yaml"); let second = dir.0.join("second.yaml"); - fs::write(&first, "disabled_services: [plex]\n").unwrap(); - fs::write(&second, "disabled_services: [jellyfin]\n").unwrap(); - assert!(!Config::load_from(&first).unwrap().is_enabled("plex")); - assert!(Config::load_from(&second).unwrap().is_enabled("plex")); + fs::write(&first, "disabled_services: [plex]\n")?; + fs::write(&second, "disabled_services: [jellyfin]\n")?; + assert!(!Config::load_from(&first)?.is_enabled("plex")); + assert!(Config::load_from(&second)?.is_enabled("plex")); + Ok(()) }