//! Integration tests for k8shost API Server //! //! These tests verify end-to-end functionality including: //! - Pod lifecycle (create, get, list, delete) //! - Service exposure and cluster IP allocation //! - Multi-tenant isolation //! - IAM authentication and authorization use k8shost_proto::{ pod_service_client::PodServiceClient, service_service_client::ServiceServiceClient, node_service_client::NodeServiceClient, Container, ContainerPort, CreatePodRequest, CreateServiceRequest, DeletePodRequest, DeleteServiceRequest, GetPodRequest, GetServiceRequest, ListPodsRequest, ListServicesRequest, ObjectMeta, Pod, PodSpec, Service, ServicePort, ServiceSpec, }; use std::collections::HashMap; use tonic::metadata::MetadataValue; use tonic::transport::Channel; use tonic::{Request, Status}; // Type alias for intercepted service with our authentication closure type AuthInterceptor = fn(Request<()>) -> Result, Status>; /// Test configuration struct TestConfig { server_addr: String, flaredb_addr: String, iam_addr: String, } impl TestConfig { fn from_env() -> Self { Self { server_addr: std::env::var("K8SHOST_SERVER_ADDR") .unwrap_or_else(|_| "http://127.0.0.1:6443".to_string()), flaredb_addr: std::env::var("FLAREDB_PD_ADDR") .unwrap_or_else(|_| "127.0.0.1:2379".to_string()), iam_addr: std::env::var("IAM_SERVER_ADDR") .unwrap_or_else(|_| "http://127.0.0.1:50051".to_string()), } } } /// Helper to create an authenticated gRPC client with bearer token async fn create_authenticated_pod_client( token: &str, ) -> Result) -> Result, Status> + Clone>>, Box> { let config = TestConfig::from_env(); let channel = Channel::from_shared(config.server_addr.clone())? .connect() .await?; let token_value = format!("Bearer {}", token); let token_metadata: MetadataValue<_> = token_value.parse()?; // Create a channel-based client with interceptor let client = PodServiceClient::with_interceptor( channel, move |mut req: Request<()>| -> Result, Status> { req.metadata_mut() .insert("authorization", token_metadata.clone()); Ok(req) }, ); Ok(client) } /// Helper to create an authenticated service client async fn create_authenticated_service_client( token: &str, ) -> Result) -> Result, Status> + Clone>>, Box> { let config = TestConfig::from_env(); let channel = Channel::from_shared(config.server_addr.clone())? .connect() .await?; let token_value = format!("Bearer {}", token); let token_metadata: MetadataValue<_> = token_value.parse()?; let client = ServiceServiceClient::with_interceptor( channel, move |mut req: Request<()>| -> Result, Status> { req.metadata_mut() .insert("authorization", token_metadata.clone()); Ok(req) }, ); Ok(client) } /// Mock token generator for testing /// In a real setup, this would call IAM to issue tokens fn generate_mock_token(org_id: &str, project_id: &str, principal: &str) -> String { // For testing purposes, we'll use a simple format // In production, this should be a proper JWT from IAM format!("mock-token-{}-{}-{}", org_id, project_id, principal) } /// Helper to create a test pod spec fn create_test_pod_spec( name: &str, namespace: &str, org_id: &str, project_id: &str, ) -> Pod { Pod { metadata: Some(ObjectMeta { name: name.to_string(), namespace: Some(namespace.to_string()), uid: None, resource_version: None, creation_timestamp: None, labels: HashMap::from([("app".to_string(), "test".to_string())]), annotations: HashMap::new(), org_id: Some(org_id.to_string()), project_id: Some(project_id.to_string()), }), spec: Some(PodSpec { containers: vec![Container { name: "nginx".to_string(), image: "nginx:latest".to_string(), command: vec![], args: vec![], ports: vec![ContainerPort { name: Some("http".to_string()), container_port: 80, protocol: Some("TCP".to_string()), }], env: vec![], }], restart_policy: Some("Always".to_string()), node_name: None, }), status: None, } } /// Helper to create a test service spec fn create_test_service_spec( name: &str, namespace: &str, org_id: &str, project_id: &str, ) -> Service { Service { metadata: Some(ObjectMeta { name: name.to_string(), namespace: Some(namespace.to_string()), uid: None, resource_version: None, creation_timestamp: None, labels: HashMap::new(), annotations: HashMap::new(), org_id: Some(org_id.to_string()), project_id: Some(project_id.to_string()), }), spec: Some(ServiceSpec { ports: vec![ServicePort { name: Some("http".to_string()), port: 80, target_port: Some(80), protocol: Some("TCP".to_string()), }], selector: HashMap::from([("app".to_string(), "test".to_string())]), cluster_ip: None, r#type: Some("ClusterIP".to_string()), }), status: None, } } /// Test 1: Pod Lifecycle /// Create, get, list, and delete a pod #[tokio::test] #[ignore] // Run with --ignored flag when server is running async fn test_pod_lifecycle() -> Result<(), Box> { // Generate test token let token = generate_mock_token("org-test", "project-test", "user-1"); let mut client = create_authenticated_pod_client(&token).await?; // Create a pod let pod_name = "test-pod-lifecycle"; let namespace = "default"; let pod = create_test_pod_spec(pod_name, namespace, "org-test", "project-test"); let create_response = client .create_pod(Request::new(CreatePodRequest { pod: Some(pod) })) .await?; let created_pod = create_response .into_inner() .pod .expect("Created pod should be returned"); println!("Created pod: {:?}", created_pod.metadata); // Verify pod has UID and creation timestamp assert!(created_pod.metadata.as_ref().unwrap().uid.is_some()); assert!(created_pod .metadata .as_ref() .unwrap() .creation_timestamp .is_some()); // Get the pod let get_response = client .get_pod(Request::new(GetPodRequest { name: pod_name.to_string(), namespace: namespace.to_string(), })) .await?; let fetched_pod = get_response .into_inner() .pod .expect("Fetched pod should be returned"); assert_eq!( &fetched_pod.metadata.as_ref().unwrap().name, pod_name ); // List pods let list_response = client .list_pods(Request::new(ListPodsRequest { namespace: Some(namespace.to_string()), label_selector: HashMap::new(), })) .await?; let pods = list_response.into_inner().items; assert!( pods.iter() .any(|p| { if let Some(meta) = &p.metadata { return &meta.name == pod_name; } false }), "Created pod should be in the list" ); // Delete the pod let delete_response = client .delete_pod(Request::new(DeletePodRequest { name: pod_name.to_string(), namespace: namespace.to_string(), })) .await?; assert!( delete_response.into_inner().success, "Pod should be deleted successfully" ); // Verify pod is deleted (get should fail) let get_result = client .get_pod(Request::new(GetPodRequest { name: pod_name.to_string(), namespace: namespace.to_string(), })) .await; assert!( get_result.is_err(), "Get should fail after pod is deleted" ); Ok(()) } /// Test 2: Service Exposure /// Create a service and verify cluster IP allocation #[tokio::test] #[ignore] // Run with --ignored flag when server is running async fn test_service_exposure() -> Result<(), Box> { let token = generate_mock_token("org-test", "project-test", "user-1"); let mut client = create_authenticated_service_client(&token).await?; // Create a service let service_name = "test-service-exposure"; let namespace = "default"; let service = create_test_service_spec(service_name, namespace, "org-test", "project-test"); let create_response = client .create_service(Request::new(CreateServiceRequest { service: Some(service), })) .await?; let created_service = create_response .into_inner() .service .expect("Created service should be returned"); println!("Created service: {:?}", created_service.metadata); // Verify service has cluster IP allocated assert!( created_service .spec .as_ref() .unwrap() .cluster_ip .is_some(), "Cluster IP should be allocated" ); let cluster_ip = created_service.spec.as_ref().unwrap().cluster_ip.clone().unwrap(); println!("Allocated cluster IP: {}", cluster_ip); // Get the service let get_response = client .get_service(Request::new(GetServiceRequest { name: service_name.to_string(), namespace: namespace.to_string(), })) .await?; let fetched_service = get_response .into_inner() .service .expect("Fetched service should be returned"); assert_eq!( &fetched_service.metadata.as_ref().unwrap().name, service_name ); assert_eq!( fetched_service.spec.as_ref().unwrap().cluster_ip, Some(cluster_ip) ); // List services let list_response = client .list_services(Request::new(ListServicesRequest { namespace: Some(namespace.to_string()), })) .await?; let services = list_response.into_inner().items; assert!( services .iter() .any(|s| { if let Some(meta) = &s.metadata { return &meta.name == service_name; } false }), "Created service should be in the list" ); // Delete the service let delete_response = client .delete_service(Request::new(DeleteServiceRequest { name: service_name.to_string(), namespace: namespace.to_string(), })) .await?; assert!( delete_response.into_inner().success, "Service should be deleted successfully" ); Ok(()) } /// Test 3: Multi-Tenant Isolation /// Verify that resources from one tenant cannot be accessed by another #[tokio::test] #[ignore] // Run with --ignored flag when server is running async fn test_multi_tenant_isolation() -> Result<(), Box> { // Create pod in org-A with token-A let token_a = generate_mock_token("org-a", "project-a", "user-a"); let mut client_a = create_authenticated_pod_client(&token_a).await?; let pod_name = "test-isolation-pod"; let namespace = "default"; let pod = create_test_pod_spec(pod_name, namespace, "org-a", "project-a"); let create_response = client_a .create_pod(Request::new(CreatePodRequest { pod: Some(pod) })) .await?; println!( "Created pod in org-a: {:?}", create_response.into_inner().pod ); // Try to get the pod with token-B (different org) let token_b = generate_mock_token("org-b", "project-b", "user-b"); let mut client_b = create_authenticated_pod_client(&token_b).await?; let get_result = client_b .get_pod(Request::new(GetPodRequest { name: pod_name.to_string(), namespace: namespace.to_string(), })) .await; // Should return NotFound because the pod belongs to org-a, not org-b assert!( get_result.is_err(), "Token from org-b should not be able to access pod from org-a" ); if let Err(status) = get_result { assert_eq!( status.code(), tonic::Code::NotFound, "Should return NotFound status" ); } // List pods with token-B should return empty list let list_response = client_b .list_pods(Request::new(ListPodsRequest { namespace: Some(namespace.to_string()), label_selector: HashMap::new(), })) .await?; let pods = list_response.into_inner().items; assert!( !pods .iter() .any(|p| { if let Some(meta) = &p.metadata { return &meta.name == pod_name; } false }), "Token from org-b should not see pods from org-a" ); // Cleanup: delete pod with token-A let delete_response = client_a .delete_pod(Request::new(DeletePodRequest { name: pod_name.to_string(), namespace: namespace.to_string(), })) .await?; assert!(delete_response.into_inner().success); Ok(()) } /// Test 4: Invalid Token Handling /// Verify that requests without valid tokens are rejected #[tokio::test] #[ignore] // Run with --ignored flag when server is running async fn test_invalid_token_handling() -> Result<(), Box> { // Try to create client with invalid token let invalid_token = "invalid-token-xyz"; let mut client = create_authenticated_pod_client(invalid_token).await?; let pod_name = "test-invalid-token-pod"; let namespace = "default"; let pod = create_test_pod_spec(pod_name, namespace, "org-test", "project-test"); // Attempt to create pod should fail with Unauthenticated let create_result = client .create_pod(Request::new(CreatePodRequest { pod: Some(pod) })) .await; assert!( create_result.is_err(), "Request with invalid token should fail" ); if let Err(status) = create_result { assert_eq!( status.code(), tonic::Code::Unauthenticated, "Should return Unauthenticated status" ); } Ok(()) } /// Test 5: Missing Authorization Header /// Verify that requests without Authorization header are rejected #[tokio::test] #[ignore] // Run with --ignored flag when server is running async fn test_missing_authorization() -> Result<(), Box> { let config = TestConfig::from_env(); let channel = Channel::from_shared(config.server_addr.clone())? .connect() .await?; let mut client = PodServiceClient::new(channel); let pod_name = "test-no-auth-pod"; let namespace = "default"; let pod = create_test_pod_spec(pod_name, namespace, "org-test", "project-test"); // Attempt to create pod without authorization header should fail let create_result = client .create_pod(Request::new(CreatePodRequest { pod: Some(pod) })) .await; assert!( create_result.is_err(), "Request without authorization should fail" ); if let Err(status) = create_result { assert_eq!( status.code(), tonic::Code::Unauthenticated, "Should return Unauthenticated status" ); } Ok(()) }