//! Integration tests for PrismNET control-plane //! //! These tests validate the full E2E flow from VPC creation through //! DHCP, ACL enforcement, Gateway Router, and SNAT configuration. use prismnet_server::ovn::{build_acl_match, calculate_priority, OvnClient}; use prismnet_types::{ DhcpOptions, IpProtocol, Port, RuleDirection, SecurityGroup, SecurityGroupId, SecurityGroupRule, SubnetId, Vpc, }; /// Test Scenario 1: Full Control-Plane Flow /// /// Validates the complete lifecycle: /// VPC → Subnet+DHCP → Port → SecurityGroup+ACL → Router+SNAT #[tokio::test] async fn test_full_control_plane_flow() { // Setup: Create mock OvnClient let ovn = OvnClient::new_mock(); // 1. Create VPC (logical switch) let vpc = Vpc::new("test-vpc", "org-1", "proj-1", "10.0.0.0/16"); ovn.create_logical_switch(&vpc.id, &vpc.cidr_block) .await .unwrap(); // 2. Create Subnet with DHCP options let dhcp_opts = DhcpOptions { cidr: "10.0.0.0/24".to_string(), router: Some("10.0.0.1".to_string()), dns_servers: vec!["8.8.8.8".to_string(), "8.8.4.4".to_string()], lease_time: 86400, domain_name: Some("cloud.local".to_string()), }; let dhcp_uuid = ovn .create_dhcp_options("10.0.0.0/24", &dhcp_opts) .await .unwrap(); // 3. Create Port attached to Subnet let mut port = Port::new("test-port", SubnetId::new()); port.ip_address = Some("10.0.0.5".to_string()); ovn.create_logical_switch_port(&port, &vpc.id, port.ip_address.as_ref().unwrap()) .await .unwrap(); // 4. Bind DHCP to port let lsp_name = format!("port-{}", port.id); ovn.set_lsp_dhcp_options(&lsp_name, &dhcp_uuid) .await .unwrap(); // 5. Create SecurityGroup with rules let sg = SecurityGroup::new("web-sg", "org-1", "proj-1"); // SSH rule (ingress, TCP/22 from anywhere) let mut ssh_rule = SecurityGroupRule::new(sg.id, RuleDirection::Ingress, IpProtocol::Tcp); ssh_rule.port_range_min = Some(22); ssh_rule.port_range_max = Some(22); ssh_rule.remote_cidr = Some("0.0.0.0/0".to_string()); // HTTP rule (ingress, TCP/80) let mut http_rule = SecurityGroupRule::new(sg.id, RuleDirection::Ingress, IpProtocol::Tcp); http_rule.port_range_min = Some(80); http_rule.port_range_max = Some(80); http_rule.remote_cidr = Some("0.0.0.0/0".to_string()); // 6. Apply SecurityGroup → create ACLs let ssh_match = build_acl_match(&ssh_rule, Some(&lsp_name)); let ssh_priority = calculate_priority(&ssh_rule); let ssh_acl_key = ovn .create_acl(&sg.id, &ssh_rule, &vpc.id, &ssh_match, ssh_priority) .await .unwrap(); let http_match = build_acl_match(&http_rule, Some(&lsp_name)); let http_priority = calculate_priority(&http_rule); let http_acl_key = ovn .create_acl(&sg.id, &http_rule, &vpc.id, &http_match, http_priority) .await .unwrap(); // 7. Create Gateway Router let router_id = ovn.create_logical_router("vpc-router").await.unwrap(); // 8. Attach router to VPC let router_port_id = ovn .add_router_port(&router_id, &vpc.id, "10.0.0.1/24", "02:00:00:00:00:01") .await .unwrap(); // 9. Configure SNAT ovn.configure_snat(&router_id, "203.0.113.10", "10.0.0.0/24") .await .unwrap(); // 10. ASSERTIONS: Verify mock state let state = ovn.mock_state().unwrap(); let guard = state.lock().await; // Verify VPC exists assert!(guard.has_logical_switch(&vpc.id)); // Verify DHCP options exist assert!(guard.dhcp_options_exists(&dhcp_uuid)); // Verify port has DHCP binding assert!(guard.port_has_dhcp(&lsp_name)); // Verify port is attached assert!(guard.port_attached(&port.id)); // Verify ACLs exist with correct match expressions assert!(guard.acl_exists(&ssh_acl_key)); assert!(guard.acl_exists(&http_acl_key)); let ssh_acl_match = guard.get_acl_match(&ssh_acl_key).unwrap(); assert!(ssh_acl_match.contains(&format!("inport == \"{}\"", lsp_name))); assert!(ssh_acl_match.contains("tcp.dst == 22")); assert!(ssh_acl_match.contains("ip4.src == 0.0.0.0/0")); let http_acl_match = guard.get_acl_match(&http_acl_key).unwrap(); assert!(http_acl_match.contains("tcp.dst == 80")); // Verify router exists assert!(guard.router_exists(&router_id)); // Verify router port attached assert!(guard.router_port_exists(&router_port_id)); assert_eq!(guard.get_router_port_count(&router_id), 1); // Verify SNAT rule configured assert!(guard.snat_rule_exists(&router_id, "203.0.113.10")); } /// Test Scenario 2: Multi-Tenant Isolation /// /// Ensures that two VPCs are properly isolated from each other #[tokio::test] async fn test_multi_tenant_isolation() { let ovn = OvnClient::new_mock(); // Tenant A let vpc_a = Vpc::new("tenant-a-vpc", "org-a", "proj-a", "10.0.0.0/16"); ovn.create_logical_switch(&vpc_a.id, &vpc_a.cidr_block) .await .unwrap(); let mut port_a = Port::new("tenant-a-port", SubnetId::new()); port_a.ip_address = Some("10.0.0.10".to_string()); ovn.create_logical_switch_port(&port_a, &vpc_a.id, port_a.ip_address.as_ref().unwrap()) .await .unwrap(); // Tenant B let vpc_b = Vpc::new("tenant-b-vpc", "org-b", "proj-b", "10.1.0.0/16"); ovn.create_logical_switch(&vpc_b.id, &vpc_b.cidr_block) .await .unwrap(); let mut port_b = Port::new("tenant-b-port", SubnetId::new()); port_b.ip_address = Some("10.1.0.10".to_string()); ovn.create_logical_switch_port(&port_b, &vpc_b.id, port_b.ip_address.as_ref().unwrap()) .await .unwrap(); // Verify: Each VPC has separate logical switch let state = ovn.mock_state().unwrap(); let guard = state.lock().await; assert!(guard.has_logical_switch(&vpc_a.id)); assert!(guard.has_logical_switch(&vpc_b.id)); // Verify: Ports isolated to their VPCs assert!(guard.port_attached(&port_a.id)); assert!(guard.port_attached(&port_b.id)); // Verify ports are in the correct VPCs let port_a_state = guard.logical_ports.get(&port_a.id).unwrap(); assert_eq!(port_a_state.logical_switch, vpc_a.id); assert_eq!(port_a_state.ip, "10.0.0.10"); let port_b_state = guard.logical_ports.get(&port_b.id).unwrap(); assert_eq!(port_b_state.logical_switch, vpc_b.id); assert_eq!(port_b_state.ip, "10.1.0.10"); } /// Test Scenario 3: ACL Priority Ordering /// /// Validates that more specific ACL rules get higher priority #[tokio::test] async fn test_acl_priority_ordering() { let ovn = OvnClient::new_mock(); let vpc = Vpc::new("test-vpc", "org-1", "proj-1", "10.0.0.0/16"); ovn.create_logical_switch(&vpc.id, &vpc.cidr_block) .await .unwrap(); let sg_id = SecurityGroupId::new(); // Rule 1: Protocol only (priority 700) let rule_protocol_only = SecurityGroupRule::new(sg_id, RuleDirection::Ingress, IpProtocol::Tcp); let priority_protocol = calculate_priority(&rule_protocol_only); assert_eq!(priority_protocol, 700); // Rule 2: Protocol + port (priority 800) let mut rule_with_port = SecurityGroupRule::new(sg_id, RuleDirection::Ingress, IpProtocol::Tcp); rule_with_port.port_range_min = Some(80); rule_with_port.port_range_max = Some(80); let priority_port = calculate_priority(&rule_with_port); assert_eq!(priority_port, 800); // Rule 3: Protocol + CIDR (priority 800) let mut rule_with_cidr = SecurityGroupRule::new(sg_id, RuleDirection::Ingress, IpProtocol::Tcp); rule_with_cidr.remote_cidr = Some("10.0.0.0/8".to_string()); let priority_cidr = calculate_priority(&rule_with_cidr); assert_eq!(priority_cidr, 800); // Rule 4: Protocol + port + CIDR (priority 1000 - most specific) let mut rule_full = SecurityGroupRule::new(sg_id, RuleDirection::Ingress, IpProtocol::Tcp); rule_full.port_range_min = Some(443); rule_full.port_range_max = Some(443); rule_full.remote_cidr = Some("192.168.0.0/16".to_string()); let priority_full = calculate_priority(&rule_full); assert_eq!(priority_full, 1000); // Rule 5: Any protocol (priority 600 - least specific) let rule_any = SecurityGroupRule::new(sg_id, RuleDirection::Ingress, IpProtocol::Any); let priority_any = calculate_priority(&rule_any); assert_eq!(priority_any, 600); // Verify ordering: full > port/cidr > protocol > any assert!(priority_full > priority_port); assert!(priority_port > priority_protocol); assert!(priority_protocol > priority_any); } /// Test Scenario 4: Router Cascade Deletion /// /// Validates that deleting a router also removes associated router ports and SNAT rules #[tokio::test] async fn test_router_cascade_deletion() { let ovn = OvnClient::new_mock(); let vpc = Vpc::new("test-vpc", "org-1", "proj-1", "10.0.0.0/16"); // Create VPC ovn.create_logical_switch(&vpc.id, &vpc.cidr_block) .await .unwrap(); // Create router let router_id = ovn.create_logical_router("test-router").await.unwrap(); // Add router port let port_id = ovn .add_router_port(&router_id, &vpc.id, "10.0.0.1/24", "02:00:00:00:00:01") .await .unwrap(); // Configure SNAT ovn.configure_snat(&router_id, "203.0.113.10", "10.0.0.0/24") .await .unwrap(); // Verify everything exists let state = ovn.mock_state().unwrap(); { let guard = state.lock().await; assert!(guard.router_exists(&router_id)); assert!(guard.router_port_exists(&port_id)); assert!(guard.snat_rule_exists(&router_id, "203.0.113.10")); } // Delete router ovn.delete_logical_router(&router_id).await.unwrap(); // Verify cascade deletion let guard = state.lock().await; assert!(!guard.router_exists(&router_id)); assert!(!guard.router_port_exists(&port_id)); assert!(!guard.snat_rule_exists(&router_id, "203.0.113.10")); } /// Test Scenario 5: DHCP Option Updates /// /// Validates that DHCP options can be created, bound to ports, and deleted #[tokio::test] async fn test_dhcp_options_lifecycle() { let ovn = OvnClient::new_mock(); let vpc = Vpc::new("test-vpc", "org-1", "proj-1", "10.0.0.0/16"); // Create VPC ovn.create_logical_switch(&vpc.id, &vpc.cidr_block) .await .unwrap(); // Create DHCP options let dhcp_opts = DhcpOptions { cidr: "10.0.0.0/24".to_string(), router: Some("10.0.0.1".to_string()), dns_servers: vec!["8.8.8.8".to_string()], lease_time: 3600, domain_name: Some("test.local".to_string()), }; let dhcp_uuid = ovn .create_dhcp_options("10.0.0.0/24", &dhcp_opts) .await .unwrap(); // Create port let mut port = Port::new("test-port", SubnetId::new()); port.ip_address = Some("10.0.0.5".to_string()); ovn.create_logical_switch_port(&port, &vpc.id, port.ip_address.as_ref().unwrap()) .await .unwrap(); // Bind DHCP to port let lsp_name = format!("port-{}", port.id); ovn.set_lsp_dhcp_options(&lsp_name, &dhcp_uuid) .await .unwrap(); // Verify DHCP options exist and are bound let state = ovn.mock_state().unwrap(); { let guard = state.lock().await; assert!(guard.dhcp_options_exists(&dhcp_uuid)); assert!(guard.port_has_dhcp(&lsp_name)); } // Delete DHCP options ovn.delete_dhcp_options(&dhcp_uuid).await.unwrap(); // Verify deletion let guard = state.lock().await; assert!(!guard.dhcp_options_exists(&dhcp_uuid)); } /// Test Scenario 6: SecurityGroup Rule Lifecycle /// /// Validates adding and removing ACL rules #[tokio::test] async fn test_security_group_rule_lifecycle() { let ovn = OvnClient::new_mock(); let vpc = Vpc::new("test-vpc", "org-1", "proj-1", "10.0.0.0/16"); ovn.create_logical_switch(&vpc.id, &vpc.cidr_block) .await .unwrap(); let sg = SecurityGroup::new("test-sg", "org-1", "proj-1"); // Add SSH rule let ssh_rule = SecurityGroupRule::tcp_port(sg.id, RuleDirection::Ingress, 22, "0.0.0.0/0"); let ssh_match = build_acl_match(&ssh_rule, None); let ssh_priority = calculate_priority(&ssh_rule); let acl_key = ovn .create_acl(&sg.id, &ssh_rule, &vpc.id, &ssh_match, ssh_priority) .await .unwrap(); // Verify ACL exists let state = ovn.mock_state().unwrap(); { let guard = state.lock().await; assert!(guard.acl_exists(&acl_key)); let match_expr = guard.get_acl_match(&acl_key).unwrap(); assert!(match_expr.contains("tcp")); assert!(match_expr.contains("tcp.dst == 22")); } // Remove ACL ovn.delete_acl(&ssh_rule.id).await.unwrap(); // Verify deletion let guard = state.lock().await; assert!(!guard.acl_exists(&acl_key)); } /// Test Scenario 7: VPC Deletion Cascades /// /// Validates that deleting a VPC removes all associated ports and ACLs #[tokio::test] async fn test_vpc_deletion_cascades() { let ovn = OvnClient::new_mock(); let vpc = Vpc::new("test-vpc", "org-1", "proj-1", "10.0.0.0/16"); // Create VPC ovn.create_logical_switch(&vpc.id, &vpc.cidr_block) .await .unwrap(); // Create ports let mut port1 = Port::new("port1", SubnetId::new()); port1.ip_address = Some("10.0.0.5".to_string()); ovn.create_logical_switch_port(&port1, &vpc.id, port1.ip_address.as_ref().unwrap()) .await .unwrap(); let mut port2 = Port::new("port2", SubnetId::new()); port2.ip_address = Some("10.0.0.6".to_string()); ovn.create_logical_switch_port(&port2, &vpc.id, port2.ip_address.as_ref().unwrap()) .await .unwrap(); // Create ACL let sg_id = SecurityGroupId::new(); let rule = SecurityGroupRule::tcp_port(sg_id, RuleDirection::Ingress, 80, "0.0.0.0/0"); let match_expr = build_acl_match(&rule, None); let priority = calculate_priority(&rule); let acl_key = ovn .create_acl(&sg_id, &rule, &vpc.id, &match_expr, priority) .await .unwrap(); // Verify everything exists let state = ovn.mock_state().unwrap(); { let guard = state.lock().await; assert!(guard.has_logical_switch(&vpc.id)); assert!(guard.port_attached(&port1.id)); assert!(guard.port_attached(&port2.id)); assert!(guard.acl_exists(&acl_key)); } // Delete VPC ovn.delete_logical_switch(&vpc.id).await.unwrap(); // Verify cascade deletion let guard = state.lock().await; assert!(!guard.has_logical_switch(&vpc.id)); assert!(!guard.port_attached(&port1.id)); assert!(!guard.port_attached(&port2.id)); assert!(!guard.acl_exists(&acl_key)); } /// Test Scenario 8: Multiple Routers and SNAT Rules /// /// Validates that a single router can have multiple SNAT rules #[tokio::test] async fn test_multiple_snat_rules() { let ovn = OvnClient::new_mock(); // Create router let router_id = ovn.create_logical_router("multi-snat-router").await.unwrap(); // Add multiple SNAT rules for different subnets ovn.configure_snat(&router_id, "203.0.113.10", "10.0.0.0/24") .await .unwrap(); ovn.configure_snat(&router_id, "203.0.113.11", "10.1.0.0/24") .await .unwrap(); ovn.configure_snat(&router_id, "203.0.113.12", "10.2.0.0/24") .await .unwrap(); // Verify all SNAT rules exist let state = ovn.mock_state().unwrap(); let guard = state.lock().await; assert!(guard.snat_rule_exists(&router_id, "203.0.113.10")); assert!(guard.snat_rule_exists(&router_id, "203.0.113.11")); assert!(guard.snat_rule_exists(&router_id, "203.0.113.12")); // Verify total SNAT rule count let snat_count = guard .snat_rules .iter() .filter(|rule| rule.router_id == router_id) .count(); assert_eq!(snat_count, 3); } /// Test Scenario 9: ACL Match Expression Validation /// /// Validates that ACL match expressions are correctly built for different scenarios #[tokio::test] async fn test_acl_match_expression_validation() { let sg_id = SecurityGroupId::new(); // Test 1: TCP with port range let mut tcp_range_rule = SecurityGroupRule::new(sg_id, RuleDirection::Ingress, IpProtocol::Tcp); tcp_range_rule.port_range_min = Some(8000); tcp_range_rule.port_range_max = Some(9000); tcp_range_rule.remote_cidr = Some("192.168.0.0/16".to_string()); let match_expr = build_acl_match(&tcp_range_rule, Some("port-123")); assert!(match_expr.contains("inport == \"port-123\"")); assert!(match_expr.contains("tcp")); assert!(match_expr.contains("tcp.dst >= 8000")); assert!(match_expr.contains("tcp.dst <= 9000")); assert!(match_expr.contains("ip4.src == 192.168.0.0/16")); // Test 2: UDP single port let mut udp_rule = SecurityGroupRule::new(sg_id, RuleDirection::Ingress, IpProtocol::Udp); udp_rule.port_range_min = Some(53); udp_rule.port_range_max = Some(53); let match_expr = build_acl_match(&udp_rule, None); assert!(match_expr.contains("udp")); assert!(match_expr.contains("udp.dst == 53")); assert!(!match_expr.contains("inport")); // Test 3: ICMP (no port) let icmp_rule = SecurityGroupRule::new(sg_id, RuleDirection::Ingress, IpProtocol::Icmp); let match_expr = build_acl_match(&icmp_rule, None); assert!(match_expr.contains("icmp4")); assert!(!match_expr.contains("tcp")); assert!(!match_expr.contains("udp")); // Test 4: Egress direction (different port field) let mut egress_rule = SecurityGroupRule::new(sg_id, RuleDirection::Egress, IpProtocol::Tcp); egress_rule.port_range_min = Some(443); egress_rule.port_range_max = Some(443); egress_rule.remote_cidr = Some("0.0.0.0/0".to_string()); let match_expr = build_acl_match(&egress_rule, Some("port-456")); assert!(match_expr.contains("outport == \"port-456\"")); assert!(match_expr.contains("ip4.dst == 0.0.0.0/0")); // dst for egress // Test 5: Any protocol let any_rule = SecurityGroupRule::new(sg_id, RuleDirection::Ingress, IpProtocol::Any); let match_expr = build_acl_match(&any_rule, None); assert!(match_expr.contains("ip4")); assert!(!match_expr.contains("tcp")); assert!(!match_expr.contains("udp")); assert!(!match_expr.contains("icmp")); }