422 lines
13 KiB
Rust
Raw Normal View History

//! Unit tests for the authentication plugin.
use crate::auth::{self, Claims, VerificationResult};
use crate::error::{AuthError, AuthErrorKind};
use crate::helpers::{SessionInfo, UserInfo, VerifyResponse};
use crate::keyring;
use nu_protocol::Span;
// =============================================================================
// Auth Module Tests
// =============================================================================
#[test]
fn test_verification_result_success() {
let claims = Claims {
sub: "user-123".to_string(),
username: "admin".to_string(),
email: "admin@example.com".to_string(),
roles: vec!["admin".to_string(), "user".to_string()],
exp: chrono::Utc::now().timestamp() + 3600,
iat: chrono::Utc::now().timestamp(),
nbf: 0,
jti: "jti-123".to_string(),
iss: "provisioning".to_string(),
aud: "cli".to_string(),
};
let result = VerificationResult::success(claims);
assert!(result.valid);
assert!(result.claims.is_some());
assert!(result.error.is_none());
assert!(result.expires_in.is_some());
assert!(result.expires_in.unwrap() > 0);
}
#[test]
fn test_verification_result_failure() {
let result = VerificationResult::failure("Token expired");
assert!(!result.valid);
assert!(result.claims.is_none());
assert!(result.error.is_some());
assert_eq!(result.error.unwrap(), "Token expired");
}
#[test]
fn test_decode_claims_invalid_token_format() {
let result = auth::decode_claims_unverified("invalid");
assert!(result.is_err());
let error = result.unwrap_err();
assert_eq!(error.kind, AuthErrorKind::InvalidToken);
}
#[test]
fn test_decode_claims_missing_parts() {
let result = auth::decode_claims_unverified("only.two");
assert!(result.is_err());
let error = result.unwrap_err();
assert_eq!(error.kind, AuthErrorKind::InvalidToken);
assert!(error.context.contains("3 parts"));
}
#[test]
fn test_decode_claims_invalid_base64() {
let result = auth::decode_claims_unverified("header.not_valid_base64!@#$.signature");
assert!(result.is_err());
}
// =============================================================================
// Error Module Tests
// =============================================================================
#[test]
fn test_auth_error_display() {
let error = AuthError::new(AuthErrorKind::InvalidCredentials, "Wrong password");
let display = format!("{}", error);
assert!(display.contains("invalid credentials"));
assert!(display.contains("Wrong password"));
}
#[test]
fn test_auth_error_with_source() {
let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
let error = AuthError::with_source(AuthErrorKind::KeyringError, "keyring failed", io_error);
let display = format!("{}", error);
assert!(display.contains("keyring"));
assert!(display.contains("caused by"));
}
#[test]
fn test_auth_error_convenience_constructors() {
let error = AuthError::invalid_credentials("bad password");
assert_eq!(error.kind, AuthErrorKind::InvalidCredentials);
let error = AuthError::token_expired("expired 5 minutes ago");
assert_eq!(error.kind, AuthErrorKind::TokenExpired);
let error = AuthError::network_error("connection refused");
assert_eq!(error.kind, AuthErrorKind::NetworkError);
let error = AuthError::server_error("500 Internal Server Error");
assert_eq!(error.kind, AuthErrorKind::ServerError);
let error = AuthError::mfa_failed("invalid code");
assert_eq!(error.kind, AuthErrorKind::MfaFailed);
let error = AuthError::configuration_error("missing public key");
assert_eq!(error.kind, AuthErrorKind::ConfigurationError);
}
#[test]
fn test_auth_error_kind_display() {
assert_eq!(
AuthErrorKind::InvalidCredentials.to_string(),
"invalid credentials"
);
assert_eq!(AuthErrorKind::TokenExpired.to_string(), "token expired");
assert_eq!(AuthErrorKind::InvalidToken.to_string(), "invalid token format");
assert_eq!(AuthErrorKind::KeyringError.to_string(), "keyring operation failed");
assert_eq!(AuthErrorKind::NetworkError.to_string(), "network error");
assert_eq!(AuthErrorKind::MfaFailed.to_string(), "MFA verification failed");
}
#[test]
fn test_auth_error_to_labeled_error() {
let error = AuthError::new(AuthErrorKind::InvalidCredentials, "test error");
let labeled: nu_protocol::LabeledError = error.into();
// LabeledError was created successfully
assert!(format!("{:?}", labeled).contains("test error"));
}
// =============================================================================
// Keyring Module Tests
// =============================================================================
#[test]
fn test_get_current_username() {
let username = keyring::get_current_username();
// Should return a non-empty string
assert!(!username.is_empty());
}
#[test]
fn test_has_tokens_nonexistent_user() {
// Should return false for a user that doesn't exist
let result = keyring::has_tokens("__test_nonexistent_user_xyz_123__");
assert!(!result);
}
#[test]
fn test_remove_tokens_nonexistent_user() {
// Should not error when removing tokens for non-existent user
let result = keyring::remove_tokens("__test_nonexistent_user_xyz_123__");
assert!(result.is_ok());
}
#[test]
fn test_list_stored_users() {
// Currently returns empty list (platform-specific)
let users = keyring::list_stored_users();
// Just verify it doesn't panic
let _ = users.len();
}
// =============================================================================
// Helpers Module Tests
// =============================================================================
#[test]
fn test_user_info_to_value() {
use crate::helpers::user_info_to_value;
let user = UserInfo {
id: "user-123".to_string(),
username: "admin".to_string(),
email: "admin@example.com".to_string(),
roles: vec!["admin".to_string(), "user".to_string()],
};
let value = user_info_to_value(&user, Span::test_data());
assert!(matches!(value, nu_protocol::Value::Record { .. }));
}
#[test]
fn test_session_info_to_value() {
use crate::helpers::session_info_to_value;
let session = SessionInfo {
id: "sess-123".to_string(),
user_id: "user-123".to_string(),
username: "admin".to_string(),
roles: vec!["admin".to_string()],
created_at: "2024-01-01T00:00:00Z".to_string(),
expires_at: "2024-01-02T00:00:00Z".to_string(),
is_active: true,
ip_address: "127.0.0.1".to_string(),
user_agent: "Mozilla/5.0".to_string(),
};
let value = session_info_to_value(&session, Span::test_data());
assert!(matches!(value, nu_protocol::Value::Record { .. }));
}
#[test]
fn test_verify_response_to_value_valid() {
use crate::helpers::verify_response_to_value;
let response = VerifyResponse {
valid: true,
user_id: Some("user-123".to_string()),
username: Some("admin".to_string()),
roles: Some(vec!["admin".to_string()]),
expires_at: Some("2024-12-31T23:59:59Z".to_string()),
};
let value = verify_response_to_value(&response, Span::test_data());
assert!(matches!(value, nu_protocol::Value::Record { .. }));
}
#[test]
fn test_verify_response_to_value_invalid() {
use crate::helpers::verify_response_to_value;
let response = VerifyResponse {
valid: false,
user_id: None,
username: None,
roles: None,
expires_at: None,
};
let value = verify_response_to_value(&response, Span::test_data());
assert!(matches!(value, nu_protocol::Value::Record { .. }));
}
// =============================================================================
// Data Structure Tests
// =============================================================================
#[test]
fn test_claims_serialization() {
let claims = Claims {
sub: "user-123".to_string(),
username: "admin".to_string(),
email: "admin@example.com".to_string(),
roles: vec!["admin".to_string()],
exp: 1700000000,
iat: 1699996400,
nbf: 0,
jti: "jti-123".to_string(),
iss: "provisioning".to_string(),
aud: "cli".to_string(),
};
// Test that claims can be serialized
let json = serde_json::to_string(&claims).expect("Should serialize");
assert!(json.contains("user-123"));
assert!(json.contains("admin"));
// Test that claims can be deserialized
let deserialized: Claims = serde_json::from_str(&json).expect("Should deserialize");
assert_eq!(deserialized.sub, "user-123");
assert_eq!(deserialized.username, "admin");
}
#[test]
fn test_user_info_serialization() {
let user = UserInfo {
id: "user-123".to_string(),
username: "admin".to_string(),
email: "admin@example.com".to_string(),
roles: vec!["admin".to_string(), "user".to_string()],
};
let json = serde_json::to_string(&user).expect("Should serialize");
assert!(json.contains("user-123"));
assert!(json.contains("admin@example.com"));
let deserialized: UserInfo = serde_json::from_str(&json).expect("Should deserialize");
assert_eq!(deserialized.id, "user-123");
assert_eq!(deserialized.roles.len(), 2);
}
#[test]
fn test_session_info_serialization() {
let session = SessionInfo {
id: "sess-123".to_string(),
user_id: "user-123".to_string(),
username: "admin".to_string(),
roles: vec!["admin".to_string()],
created_at: "2024-01-01T00:00:00Z".to_string(),
expires_at: "2024-01-02T00:00:00Z".to_string(),
is_active: true,
ip_address: "".to_string(),
user_agent: "".to_string(),
};
let json = serde_json::to_string(&session).expect("Should serialize");
let deserialized: SessionInfo = serde_json::from_str(&json).expect("Should deserialize");
assert_eq!(deserialized.id, "sess-123");
assert!(deserialized.is_active);
}
#[test]
fn test_verify_response_serialization() {
let response = VerifyResponse {
valid: true,
user_id: Some("user-123".to_string()),
username: Some("admin".to_string()),
roles: Some(vec!["admin".to_string()]),
expires_at: Some("2024-12-31T23:59:59Z".to_string()),
};
let json = serde_json::to_string(&response).expect("Should serialize");
let deserialized: VerifyResponse = serde_json::from_str(&json).expect("Should deserialize");
assert!(deserialized.valid);
assert_eq!(deserialized.user_id, Some("user-123".to_string()));
}
// =============================================================================
// Plugin Structure Tests
// =============================================================================
#[test]
fn test_plugin_version() {
use crate::AuthPlugin;
use nu_plugin::Plugin;
let plugin = AuthPlugin;
let version = plugin.version();
// Version should be non-empty and match Cargo.toml
assert!(!version.is_empty());
}
#[test]
fn test_plugin_commands() {
use crate::AuthPlugin;
use nu_plugin::Plugin;
let plugin = AuthPlugin;
let commands = plugin.commands();
// Should have 6 commands
assert_eq!(commands.len(), 6);
}
#[test]
fn test_login_command_signature() {
use crate::Login;
use nu_plugin::SimplePluginCommand;
let login = Login;
assert_eq!(SimplePluginCommand::name(&login), "auth login");
let sig = SimplePluginCommand::signature(&login);
// Check required positional argument
assert!(!sig.required_positional.is_empty());
// Check examples exist
assert!(!SimplePluginCommand::examples(&login).is_empty());
}
#[test]
fn test_logout_command_signature() {
use crate::Logout;
use nu_plugin::SimplePluginCommand;
let logout = Logout;
assert_eq!(SimplePluginCommand::name(&logout), "auth logout");
assert!(!SimplePluginCommand::examples(&logout).is_empty());
}
#[test]
fn test_verify_command_signature() {
use crate::Verify;
use nu_plugin::SimplePluginCommand;
let verify = Verify;
assert_eq!(SimplePluginCommand::name(&verify), "auth verify");
assert!(!SimplePluginCommand::examples(&verify).is_empty());
}
#[test]
fn test_sessions_command_signature() {
use crate::Sessions;
use nu_plugin::SimplePluginCommand;
let sessions = Sessions;
assert_eq!(SimplePluginCommand::name(&sessions), "auth sessions");
assert!(!SimplePluginCommand::examples(&sessions).is_empty());
}
#[test]
fn test_mfa_enroll_command_signature() {
use crate::MfaEnroll;
use nu_plugin::SimplePluginCommand;
let mfa_enroll = MfaEnroll;
assert_eq!(SimplePluginCommand::name(&mfa_enroll), "auth mfa enroll");
assert!(!SimplePluginCommand::examples(&mfa_enroll).is_empty());
}
#[test]
fn test_mfa_verify_command_signature() {
use crate::MfaVerify;
use nu_plugin::SimplePluginCommand;
let mfa_verify = MfaVerify;
assert_eq!(SimplePluginCommand::name(&mfa_verify), "auth mfa verify");
assert!(!SimplePluginCommand::examples(&mfa_verify).is_empty());
}
// =============================================================================
// Constants Tests
// =============================================================================
#[test]
fn test_default_control_center_url() {
assert_eq!(auth::DEFAULT_CONTROL_CENTER_URL, "http://localhost:8081");
}