use crate::credentials::CredentialsService;
use crate::service::AuthService;
use crate::{display_device_code_instructions, get_auth_url, open_browser};
use anyhow::Result;
pub async fn handle_login(auth_url: Option<String>, binary_name: &str) -> Result<()> {
let credentials_service = CredentialsService::new();
if credentials_service.is_logged_in() {
if let Ok(Some((_, email, name))) = credentials_service.get_user_info() {
let display_name = name.unwrap_or_else(|| email.clone());
println!();
println!("Already logged in as {}", display_name);
println!();
println!("Run `{} auth logout` to log out first.", binary_name);
println!();
return Ok(());
}
}
let auth_service = match auth_url {
Some(url) => AuthService::with_auth_url(url),
None => AuthService::with_auth_url(get_auth_url()),
};
println!();
println!("Logging in to mecha10...");
println!();
let credentials = auth_service
.run_device_code_flow(|device_code| {
display_device_code_instructions(device_code);
open_browser(&device_code.verification_uri);
})
.await?;
credentials_service.save(&credentials)?;
let display_name = credentials.name.as_ref().unwrap_or(&credentials.email);
println!();
println!("Successfully logged in as {}", display_name);
println!();
println!(
"Credentials saved to {}",
credentials_service.credentials_path().display()
);
println!();
Ok(())
}
pub fn handle_logout() -> Result<()> {
let credentials_service = CredentialsService::new();
if !credentials_service.is_logged_in() {
println!();
println!("Not currently logged in.");
println!();
return Ok(());
}
let user_info = credentials_service.get_user_info()?;
credentials_service.delete()?;
println!();
if let Some((_, email, name)) = user_info {
let display_name = name.unwrap_or(email);
println!("Logged out from {}", display_name);
} else {
println!("Logged out successfully");
}
println!();
Ok(())
}
pub fn handle_whoami(verbose: bool, binary_name: &str) -> Result<()> {
let credentials_service = CredentialsService::new();
println!();
match credentials_service.load()? {
Some(creds) if creds.is_valid() => {
let display_name = creds.name.as_ref().unwrap_or(&creds.email);
println!("Logged in as: {}", display_name);
println!("Email: {}", creds.email);
println!("User ID: {}", creds.user_id);
if verbose {
println!("API Key: {}", creds.masked_api_key());
}
println!(
"Authenticated: {}",
creds.authenticated_at.format("%Y-%m-%d %H:%M:%S UTC")
);
}
Some(_) => {
println!("Credentials found but invalid");
println!();
println!("Run `{} auth login` to authenticate", binary_name);
}
None => {
println!("Not logged in");
println!();
println!("Run `{} auth login` to authenticate", binary_name);
}
}
println!();
Ok(())
}
pub fn auth_status_line() -> Result<String> {
let credentials_service = CredentialsService::new();
let line = match credentials_service.load()? {
Some(creds) if creds.is_valid() => {
let display_name = creds.name.as_ref().unwrap_or(&creds.email);
format!("Logged in as {} ({})", display_name, creds.email)
}
Some(_) => "Credentials found but invalid".to_string(),
None => "Not logged in".to_string(),
};
Ok(line)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::credentials::CredentialsService;
use crate::types::Credentials;
use crate::DEFAULT_AUTH_URL;
use chrono::Utc;
use serial_test::serial;
use tempfile::TempDir;
struct HomeGuard {
_temp_dir: TempDir,
original_home: Option<String>,
}
impl HomeGuard {
fn new() -> Self {
let temp_dir = TempDir::new().unwrap();
let original_home = std::env::var("HOME").ok();
std::env::set_var("HOME", temp_dir.path());
Self {
_temp_dir: temp_dir,
original_home,
}
}
}
impl Drop for HomeGuard {
fn drop(&mut self) {
match &self.original_home {
Some(home) => std::env::set_var("HOME", home),
None => std::env::remove_var("HOME"),
}
}
}
fn valid_credentials() -> Credentials {
Credentials {
api_key: "mecha_test123abc456def".to_string(),
user_id: "usr_test123".to_string(),
email: "test@example.com".to_string(),
name: Some("Test User".to_string()),
authenticated_at: Utc::now(),
auth_url: DEFAULT_AUTH_URL.to_string(),
}
}
fn invalid_credentials() -> Credentials {
Credentials {
api_key: "".to_string(),
user_id: "usr_test123".to_string(),
email: "invalid@example.com".to_string(),
name: None,
authenticated_at: Utc::now(),
auth_url: DEFAULT_AUTH_URL.to_string(),
}
}
#[tokio::test]
#[serial]
async fn handle_login_short_circuits_when_already_logged_in() {
let _guard = HomeGuard::new();
CredentialsService::new().save(&valid_credentials()).unwrap();
let result = handle_login(None, "mecha10").await;
assert!(result.is_ok());
assert!(CredentialsService::new().is_logged_in());
}
#[tokio::test]
#[serial]
async fn handle_login_propagates_network_error_when_device_code_request_fails() {
let _guard = HomeGuard::new();
let unreachable_url = "http://127.0.0.1:1".to_string();
let result = handle_login(Some(unreachable_url), "mecha10").await;
assert!(result.is_err());
assert!(!CredentialsService::new().is_logged_in());
}
#[tokio::test]
#[serial]
async fn handle_login_uses_env_var_auth_url_when_none_provided() {
let _guard = HomeGuard::new();
std::env::set_var(crate::AUTH_URL_ENV_VAR, "http://127.0.0.1:1");
let result = handle_login(None, "mecha10").await;
std::env::remove_var(crate::AUTH_URL_ENV_VAR);
assert!(result.is_err());
}
#[test]
#[serial]
fn handle_logout_is_noop_when_not_logged_in() {
let _guard = HomeGuard::new();
let result = handle_logout();
assert!(result.is_ok());
}
#[test]
#[serial]
fn handle_logout_deletes_credentials_when_logged_in() {
let _guard = HomeGuard::new();
let service = CredentialsService::new();
service.save(&valid_credentials()).unwrap();
assert!(service.is_logged_in());
let result = handle_logout();
assert!(result.is_ok());
assert!(!CredentialsService::new().is_logged_in());
assert!(CredentialsService::new().load().unwrap().is_none());
}
#[test]
#[serial]
fn handle_logout_succeeds_even_with_invalid_stored_credentials() {
let _guard = HomeGuard::new();
let service = CredentialsService::new();
service.save(&invalid_credentials()).unwrap();
let result = handle_logout();
assert!(result.is_ok());
}
#[test]
#[serial]
fn handle_whoami_succeeds_when_not_logged_in() {
let _guard = HomeGuard::new();
let result = handle_whoami(false, "mecha10");
assert!(result.is_ok());
}
#[test]
#[serial]
fn handle_whoami_succeeds_with_valid_credentials_and_verbose() {
let _guard = HomeGuard::new();
CredentialsService::new().save(&valid_credentials()).unwrap();
let result = handle_whoami(true, "mecha10");
assert!(result.is_ok());
}
#[test]
#[serial]
fn handle_whoami_succeeds_with_invalid_credentials() {
let _guard = HomeGuard::new();
CredentialsService::new().save(&invalid_credentials()).unwrap();
let result = handle_whoami(false, "mecha10");
assert!(result.is_ok());
}
#[test]
#[serial]
fn handle_whoami_propagates_corrupt_credentials_error() {
let _guard = HomeGuard::new();
let service = CredentialsService::new();
std::fs::create_dir_all(service.credentials_path().parent().unwrap()).unwrap();
std::fs::write(service.credentials_path(), "not valid json").unwrap();
let result = handle_whoami(false, "mecha10");
assert!(result.is_err());
}
#[test]
#[serial]
fn auth_status_line_reports_not_logged_in() {
let _guard = HomeGuard::new();
let line = auth_status_line().unwrap();
assert_eq!(line, "Not logged in");
}
#[test]
#[serial]
fn auth_status_line_reports_logged_in_user() {
let _guard = HomeGuard::new();
CredentialsService::new().save(&valid_credentials()).unwrap();
let line = auth_status_line().unwrap();
assert_eq!(line, "Logged in as Test User (test@example.com)");
}
#[test]
#[serial]
fn auth_status_line_falls_back_to_email_without_name() {
let _guard = HomeGuard::new();
let mut creds = valid_credentials();
creds.name = None;
CredentialsService::new().save(&creds).unwrap();
let line = auth_status_line().unwrap();
assert_eq!(line, "Logged in as test@example.com (test@example.com)");
}
#[test]
#[serial]
fn auth_status_line_reports_invalid_credentials() {
let _guard = HomeGuard::new();
CredentialsService::new().save(&invalid_credentials()).unwrap();
let line = auth_status_line().unwrap();
assert_eq!(line, "Credentials found but invalid");
}
}