//! 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"); }