mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-15 12:55:52 +00:00
326 lines
11 KiB
Rust
326 lines
11 KiB
Rust
use std::fmt;
|
|
|
|
/// Backend server errors
|
|
#[derive(Debug)]
|
|
pub enum BackendError {
|
|
/// Server executable not found
|
|
ExecutableNotFound(String),
|
|
/// Failed to spawn process
|
|
SpawnFailed(std::io::Error),
|
|
/// Server crashed with exit code
|
|
ServerCrashed(i32),
|
|
/// Port already in use
|
|
PortInUse(u16),
|
|
/// Authentication failed
|
|
AuthenticationFailed,
|
|
/// Invalid configuration
|
|
InvalidConfig(String),
|
|
/// Timeout waiting for server to start
|
|
StartupTimeout,
|
|
/// Network error
|
|
NetworkError(String),
|
|
/// Generic error with message
|
|
Other(String),
|
|
}
|
|
|
|
impl fmt::Display for BackendError {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::ExecutableNotFound(path) => {
|
|
write!(f, "vibetunnel executable not found at: {path}")
|
|
}
|
|
Self::SpawnFailed(err) => {
|
|
write!(f, "Failed to spawn server process: {err}")
|
|
}
|
|
Self::ServerCrashed(code) => {
|
|
write!(f, "Server crashed with exit code: {code}")
|
|
}
|
|
Self::PortInUse(port) => {
|
|
write!(f, "Port {port} is already in use")
|
|
}
|
|
Self::AuthenticationFailed => {
|
|
write!(f, "Authentication failed")
|
|
}
|
|
Self::InvalidConfig(msg) => {
|
|
write!(f, "Invalid configuration: {msg}")
|
|
}
|
|
Self::StartupTimeout => {
|
|
write!(f, "Server failed to start within timeout period")
|
|
}
|
|
Self::NetworkError(msg) => {
|
|
write!(f, "Network error: {msg}")
|
|
}
|
|
Self::Other(msg) => {
|
|
write!(f, "{msg}")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for BackendError {}
|
|
|
|
impl From<std::io::Error> for BackendError {
|
|
fn from(err: std::io::Error) -> Self {
|
|
Self::SpawnFailed(err)
|
|
}
|
|
}
|
|
|
|
/// Convert `BackendError` to a user-friendly error message
|
|
impl BackendError {
|
|
pub fn user_message(&self) -> String {
|
|
match self {
|
|
Self::ExecutableNotFound(_) => {
|
|
"The VibeTunnel server executable was not found. Please reinstall the application.".to_string()
|
|
}
|
|
Self::SpawnFailed(_) => {
|
|
"Failed to start the server process. Please check your system permissions.".to_string()
|
|
}
|
|
Self::ServerCrashed(code) => {
|
|
match code {
|
|
9 => "The server port is already in use. Please choose a different port in settings.".to_string(),
|
|
127 => "Server executable or dependencies are missing. Please reinstall the application.".to_string(),
|
|
_ => format!("The server crashed unexpectedly (code {code}). Check the logs for details.")
|
|
}
|
|
}
|
|
Self::PortInUse(port) => {
|
|
format!("Port {port} is already in use. Please choose a different port in settings.")
|
|
}
|
|
Self::AuthenticationFailed => {
|
|
"Authentication failed. Please check your credentials.".to_string()
|
|
}
|
|
Self::InvalidConfig(msg) => {
|
|
format!("Invalid configuration: {msg}")
|
|
}
|
|
Self::StartupTimeout => {
|
|
"The server took too long to start. Please try again.".to_string()
|
|
}
|
|
Self::NetworkError(_) => {
|
|
"Network error occurred. Please check your connection.".to_string()
|
|
}
|
|
Self::Other(msg) => msg.clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_display_trait() {
|
|
// Test ExecutableNotFound
|
|
let err = BackendError::ExecutableNotFound("/path/to/exe".to_string());
|
|
assert_eq!(
|
|
format!("{}", err),
|
|
"vibetunnel executable not found at: /path/to/exe"
|
|
);
|
|
|
|
// Test SpawnFailed
|
|
let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Permission denied");
|
|
let err = BackendError::SpawnFailed(io_err);
|
|
assert!(format!("{}", err).contains("Failed to spawn server process"));
|
|
|
|
// Test ServerCrashed
|
|
let err = BackendError::ServerCrashed(42);
|
|
assert_eq!(format!("{}", err), "Server crashed with exit code: 42");
|
|
|
|
// Test PortInUse
|
|
let err = BackendError::PortInUse(8080);
|
|
assert_eq!(format!("{}", err), "Port 8080 is already in use");
|
|
|
|
// Test AuthenticationFailed
|
|
let err = BackendError::AuthenticationFailed;
|
|
assert_eq!(format!("{}", err), "Authentication failed");
|
|
|
|
// Test InvalidConfig
|
|
let err = BackendError::InvalidConfig("missing field".to_string());
|
|
assert_eq!(format!("{}", err), "Invalid configuration: missing field");
|
|
|
|
// Test StartupTimeout
|
|
let err = BackendError::StartupTimeout;
|
|
assert_eq!(
|
|
format!("{}", err),
|
|
"Server failed to start within timeout period"
|
|
);
|
|
|
|
// Test NetworkError
|
|
let err = BackendError::NetworkError("connection refused".to_string());
|
|
assert_eq!(format!("{}", err), "Network error: connection refused");
|
|
|
|
// Test Other
|
|
let err = BackendError::Other("Custom error message".to_string());
|
|
assert_eq!(format!("{}", err), "Custom error message");
|
|
}
|
|
|
|
#[test]
|
|
fn test_user_message() {
|
|
// Test ExecutableNotFound
|
|
let err = BackendError::ExecutableNotFound("/some/path".to_string());
|
|
assert_eq!(
|
|
err.user_message(),
|
|
"The VibeTunnel server executable was not found. Please reinstall the application."
|
|
);
|
|
|
|
// Test SpawnFailed
|
|
let io_err = std::io::Error::new(std::io::ErrorKind::Other, "test");
|
|
let err = BackendError::SpawnFailed(io_err);
|
|
assert_eq!(
|
|
err.user_message(),
|
|
"Failed to start the server process. Please check your system permissions."
|
|
);
|
|
|
|
// Test ServerCrashed with special exit codes
|
|
let err = BackendError::ServerCrashed(9);
|
|
assert_eq!(
|
|
err.user_message(),
|
|
"The server port is already in use. Please choose a different port in settings."
|
|
);
|
|
|
|
let err = BackendError::ServerCrashed(127);
|
|
assert_eq!(
|
|
err.user_message(),
|
|
"Server executable or dependencies are missing. Please reinstall the application."
|
|
);
|
|
|
|
let err = BackendError::ServerCrashed(1);
|
|
assert_eq!(
|
|
err.user_message(),
|
|
"The server crashed unexpectedly (code 1). Check the logs for details."
|
|
);
|
|
|
|
// Test PortInUse
|
|
let err = BackendError::PortInUse(3000);
|
|
assert_eq!(
|
|
err.user_message(),
|
|
"Port 3000 is already in use. Please choose a different port in settings."
|
|
);
|
|
|
|
// Test AuthenticationFailed
|
|
let err = BackendError::AuthenticationFailed;
|
|
assert_eq!(
|
|
err.user_message(),
|
|
"Authentication failed. Please check your credentials."
|
|
);
|
|
|
|
// Test InvalidConfig
|
|
let err = BackendError::InvalidConfig("port out of range".to_string());
|
|
assert_eq!(
|
|
err.user_message(),
|
|
"Invalid configuration: port out of range"
|
|
);
|
|
|
|
// Test StartupTimeout
|
|
let err = BackendError::StartupTimeout;
|
|
assert_eq!(
|
|
err.user_message(),
|
|
"The server took too long to start. Please try again."
|
|
);
|
|
|
|
// Test NetworkError
|
|
let err = BackendError::NetworkError("DNS resolution failed".to_string());
|
|
assert_eq!(
|
|
err.user_message(),
|
|
"Network error occurred. Please check your connection."
|
|
);
|
|
|
|
// Test Other
|
|
let err = BackendError::Other("Something went wrong".to_string());
|
|
assert_eq!(err.user_message(), "Something went wrong");
|
|
}
|
|
|
|
#[test]
|
|
fn test_from_io_error() {
|
|
let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
|
|
let backend_error: BackendError = io_error.into();
|
|
|
|
match backend_error {
|
|
BackendError::SpawnFailed(err) => {
|
|
assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
|
|
}
|
|
_ => panic!("Expected SpawnFailed variant"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_error_trait_impl() {
|
|
// Verify BackendError implements std::error::Error
|
|
fn assert_error<E: std::error::Error>() {}
|
|
assert_error::<BackendError>();
|
|
}
|
|
|
|
#[test]
|
|
fn test_debug_trait() {
|
|
let err = BackendError::PortInUse(8080);
|
|
let debug_str = format!("{:?}", err);
|
|
assert!(debug_str.contains("PortInUse"));
|
|
assert!(debug_str.contains("8080"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_all_variants_have_user_messages() {
|
|
// Create one instance of each variant to ensure they all have user messages
|
|
let errors = vec![
|
|
BackendError::ExecutableNotFound("test".to_string()),
|
|
BackendError::SpawnFailed(std::io::Error::new(std::io::ErrorKind::Other, "test")),
|
|
BackendError::ServerCrashed(1),
|
|
BackendError::PortInUse(8080),
|
|
BackendError::AuthenticationFailed,
|
|
BackendError::InvalidConfig("test".to_string()),
|
|
BackendError::StartupTimeout,
|
|
BackendError::NetworkError("test".to_string()),
|
|
BackendError::Other("test".to_string()),
|
|
];
|
|
|
|
for err in errors {
|
|
// Ensure user_message() doesn't panic and returns a non-empty string
|
|
let msg = err.user_message();
|
|
assert!(!msg.is_empty());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_special_exit_codes() {
|
|
// Test all special exit codes in ServerCrashed
|
|
let special_codes = vec![
|
|
(9, "already in use"),
|
|
(127, "executable or dependencies are missing"),
|
|
];
|
|
|
|
for (code, expected_substr) in special_codes {
|
|
let err = BackendError::ServerCrashed(code);
|
|
let msg = err.user_message();
|
|
assert!(
|
|
msg.contains(expected_substr),
|
|
"Exit code {} should produce message containing '{}', got: '{}'",
|
|
code,
|
|
expected_substr,
|
|
msg
|
|
);
|
|
}
|
|
|
|
// Test non-special exit code
|
|
let err = BackendError::ServerCrashed(42);
|
|
let msg = err.user_message();
|
|
assert!(msg.contains("crashed unexpectedly"));
|
|
assert!(msg.contains("42"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_error_messages_are_helpful() {
|
|
// Ensure all user messages provide actionable guidance
|
|
let err = BackendError::ExecutableNotFound("path".to_string());
|
|
assert!(err.user_message().contains("reinstall"));
|
|
|
|
let err = BackendError::SpawnFailed(std::io::Error::new(std::io::ErrorKind::Other, ""));
|
|
assert!(err.user_message().contains("permissions"));
|
|
|
|
let err = BackendError::PortInUse(8080);
|
|
assert!(err.user_message().contains("different port"));
|
|
|
|
let err = BackendError::AuthenticationFailed;
|
|
assert!(err.user_message().contains("credentials"));
|
|
|
|
let err = BackendError::StartupTimeout;
|
|
assert!(err.user_message().contains("try again"));
|
|
}
|
|
}
|