photoncloud-monorepo/novanet/crates/novanet-server/tests/control_plane_integration.rs
centra a7ec7e2158 Add T026 practical test + k8shost to flake + workspace files
- Created T026-practical-test task.yaml for MVP smoke testing
- Added k8shost-server to flake.nix (packages, apps, overlays)
- Staged all workspace directories for nix flake build
- Updated flake.nix shellHook to include k8shost

Resolves: T026.S1 blocker (R8 - nix submodule visibility)
2025-12-09 06:07:50 +09:00

534 lines
18 KiB
Rust

//! Integration tests for NovaNET control-plane
//!
//! These tests validate the full E2E flow from VPC creation through
//! DHCP, ACL enforcement, Gateway Router, and SNAT configuration.
use novanet_server::ovn::{build_acl_match, calculate_priority, OvnClient};
use novanet_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"));
}