//! Tests for el-services. #[cfg(test)] mod tests { use std::collections::HashMap; use std::sync::Arc; use crate::{ binding::{ direct::DirectBinding, grpc::GrpcBinding, rest::RestBinding, websocket::WebSocketBinding, Binding, BindingKind, ServiceRequest, ServiceResponse, }, config::{AuthConfig, ServiceConfig}, proxy::{ServiceMethod, ServiceProxy}, registry::ServiceRegistry, }; fn rest_config(base_url: &str) -> ServiceConfig { ServiceConfig::new("UserService", BindingKind::Rest) .with_base_url(base_url) } // ── Test 1: BindingKind::from_str parses all kinds ─────────────────────── #[test] fn test_binding_kind_from_str() { assert_eq!(BindingKind::from_str("rest"), Some(BindingKind::Rest)); assert_eq!(BindingKind::from_str("websocket"), Some(BindingKind::WebSocket)); assert_eq!(BindingKind::from_str("ws"), Some(BindingKind::WebSocket)); assert_eq!(BindingKind::from_str("grpc"), Some(BindingKind::Grpc)); assert_eq!(BindingKind::from_str("direct"), Some(BindingKind::Direct)); assert_eq!(BindingKind::from_str("unknown"), None); } // ── Test 2: RestBinding derives correct HTTP methods ───────────────────── #[test] fn test_rest_http_method_derivation() { assert_eq!(RestBinding::http_method("get_user"), "GET"); assert_eq!(RestBinding::http_method("list_users"), "GET"); assert_eq!(RestBinding::http_method("create_user"), "POST"); assert_eq!(RestBinding::http_method("update_user"), "PUT"); assert_eq!(RestBinding::http_method("delete_user"), "DELETE"); assert_eq!(RestBinding::http_method("patch_user"), "PATCH"); assert_eq!(RestBinding::http_method("process"), "POST"); } // ── Test 3: RestBinding derives correct URL path ────────────────────────── #[test] fn test_rest_url_path() { let config = rest_config("https://api.example.com"); let binding = RestBinding::new(config); let mut params = HashMap::new(); params.insert("id".to_string(), "123".to_string()); let url = binding.url_path("get_user", ¶ms); assert!(url.contains("users"), "should use pluralized resource"); assert!(url.contains("123"), "should include id in URL"); } // ── Test 4: RestBinding call returns ok response ────────────────────────── #[test] fn test_rest_binding_call() { let config = rest_config("https://api.example.com"); let binding = RestBinding::new(config); let request = ServiceRequest::new("UserService", "get_user") .with_param("id", "42"); let response = binding.call(request).unwrap(); assert!(response.is_success()); assert!(response.body.contains("UserService")); } // ── Test 5: RestBinding without base_url returns error ─────────────────── #[test] fn test_rest_binding_no_base_url_errors() { let config = ServiceConfig::new("UserService", BindingKind::Rest); let binding = RestBinding::new(config); let request = ServiceRequest::new("UserService", "get_user"); let result = binding.call(request); assert!(result.is_err(), "should error without base_url"); } // ── Test 6: DirectBinding registers and calls handlers ─────────────────── #[test] fn test_direct_binding_register_and_call() { let binding = DirectBinding::new(); binding.register("UserService", "get_user", |params| { let id = params.get("id").cloned().unwrap_or_default(); Ok(format!("{{\"id\":\"{}\",\"name\":\"Alice\"}}", id)) }); let request = ServiceRequest::new("UserService", "get_user") .with_param("id", "42"); let response = binding.call(request).unwrap(); assert!(response.is_success()); assert!(response.body.contains("Alice")); assert!(response.body.contains("42")); } // ── Test 7: DirectBinding returns error for missing handler ────────────── #[test] fn test_direct_binding_missing_handler() { let binding = DirectBinding::new(); let request = ServiceRequest::new("UserService", "nonexistent"); let result = binding.call(request); assert!(result.is_err(), "should error on missing handler"); } // ── Test 8: DirectBinding::has_handler works ───────────────────────────── #[test] fn test_direct_binding_has_handler() { let binding = DirectBinding::new(); assert!(!binding.has_handler("UserService", "get_user")); binding.register("UserService", "get_user", |_| Ok("{}".into())); assert!(binding.has_handler("UserService", "get_user")); } // ── Test 9: GrpcBinding builds correct endpoint path ──────────────────── #[test] fn test_grpc_endpoint_path() { let config = ServiceConfig::new("UserService", BindingKind::Grpc) .with_base_url("http://localhost:50051"); let binding = GrpcBinding::new(config).with_package("myapp.v1"); let endpoint = binding.grpc_endpoint("get_user"); assert_eq!(endpoint, "/myapp.v1.UserService/GetUser"); } // ── Test 10: GrpcBinding without package ──────────────────────────────── #[test] fn test_grpc_endpoint_no_package() { let config = ServiceConfig::new("OrderService", BindingKind::Grpc) .with_base_url("http://localhost:50051"); let binding = GrpcBinding::new(config); let endpoint = binding.grpc_endpoint("list_orders"); assert_eq!(endpoint, "/OrderService/ListOrders"); } // ── Test 11: WebSocketBinding converts URL protocol ────────────────────── #[test] fn test_websocket_url_conversion() { let config = ServiceConfig::new("ChatService", BindingKind::WebSocket) .with_base_url("https://ws.example.com"); let binding = WebSocketBinding::new(config); let ws_url = binding.ws_url().unwrap(); assert!(ws_url.starts_with("wss://"), "should convert https to wss"); } // ── Test 12: WebSocketBinding serializes request correctly ─────────────── #[test] fn test_websocket_request_serialization() { let config = ServiceConfig::new("ChatService", BindingKind::WebSocket) .with_base_url("wss://ws.example.com"); let binding = WebSocketBinding::new(config); let request = ServiceRequest::new("ChatService", "send_message") .with_param("content", "hello"); let msg = binding.serialize_request(&request, "test-id-123"); assert!(msg.contains("test-id-123")); assert!(msg.contains("ChatService")); assert!(msg.contains("send_message")); assert!(msg.contains("hello")); } // ── Test 13: ServiceProxy::call_positional maps args to params ─────────── #[test] fn test_service_proxy_positional_call() { let binding = DirectBinding::new(); binding.register("UserService", "create_user", |params| { let name = params.get("name").cloned().unwrap_or_default(); let email = params.get("email").cloned().unwrap_or_default(); Ok(format!("{{\"name\":\"{}\",\"email\":\"{}\"}}", name, email)) }); let config = ServiceConfig::new("UserService", BindingKind::Direct); let method = ServiceMethod::new("create_user", "User") .with_params(vec!["name", "email"]); let proxy = ServiceProxy::new("UserService", config, Arc::new(binding)) .with_method(method); let response = proxy .call_positional("create_user", vec!["Alice".into(), "alice@example.com".into()]) .unwrap(); assert!(response.body.contains("Alice")); assert!(response.body.contains("alice@example.com")); } // ── Test 14: ServiceRegistry stores and retrieves services ─────────────── #[test] fn test_service_registry() { let registry = ServiceRegistry::new(); let config = ServiceConfig::new("UserService", BindingKind::Direct); let proxy = ServiceProxy::new("UserService", config, Arc::new(DirectBinding::new())); registry.register(proxy); let retrieved = registry.get("UserService").unwrap(); assert_eq!(retrieved.service_name, "UserService"); } // ── Test 15: ServiceRegistry returns error for unknown service ──────────── #[test] fn test_service_registry_not_found() { let registry = ServiceRegistry::new(); let result = registry.get("NonExistentService"); assert!(result.is_err()); } // ── Test 16: ServiceConfig with_auth sets bearer token ─────────────────── #[test] fn test_service_config_with_auth() { let config = ServiceConfig::new("UserService", BindingKind::Rest) .with_base_url("https://api.example.com") .with_auth(AuthConfig::Bearer { token_env: "API_TOKEN".into() }); match &config.auth { AuthConfig::Bearer { token_env } => assert_eq!(token_env, "API_TOKEN"), _ => panic!("expected Bearer auth"), } } // ── Test 17: ServiceResponse::is_success ───────────────────────────────── #[test] fn test_service_response_success() { assert!(ServiceResponse::ok("{}").is_success()); assert!(!ServiceResponse::error(404, "not found").is_success()); assert!(!ServiceResponse::error(500, "error").is_success()); } // ── Test 18: WebSocketBinding supports streaming ───────────────────────── #[test] fn test_websocket_supports_streaming() { let config = ServiceConfig::new("ChatService", BindingKind::WebSocket) .with_base_url("wss://ws.example.com"); let binding = WebSocketBinding::new(config); assert!(binding.supports_streaming()); } // ── Test 19: RestBinding::build_request_description includes method ─────── #[test] fn test_rest_request_description() { let config = rest_config("https://api.example.com"); let binding = RestBinding::new(config); let request = ServiceRequest::new("UserService", "create_user") .with_param("name", "Alice"); let desc = binding.build_request_description(&request); assert!(desc.contains("POST"), "create_ maps to POST"); assert!(desc.contains("users"), "resource name derived from service"); } // ── Test 20: ServiceRegistry lists all service names ───────────────────── #[test] fn test_service_registry_list() { let registry = ServiceRegistry::new(); for name in ["UserService", "OrderService", "ProductService"] { let config = ServiceConfig::new(name, BindingKind::Direct); let proxy = ServiceProxy::new(name, config, Arc::new(DirectBinding::new())); registry.register(proxy); } let mut names = registry.service_names(); names.sort(); assert_eq!(names, vec!["OrderService", "ProductService", "UserService"]); assert_eq!(registry.len(), 3); } }