//! Integration tests for FlashDNS //! //! Run with: cargo test -p flashdns-server --test integration -- --ignored use std::sync::Arc; use flashdns_api::proto::{ListRecordsRequest, ListRecordsResponse, ListZonesRequest, ListZonesResponse}; use flashdns_api::{RecordService, ZoneService}; use flashdns_server::metadata::DnsMetadataStore; use flashdns_server::record_service::RecordServiceImpl; use flashdns_server::zone_service::ZoneServiceImpl; use flashdns_types::{Record, RecordData, RecordType, Ttl, Zone, ZoneName}; use tonic::{Request, Response}; /// Test zone and record lifecycle via DnsMetadataStore #[tokio::test] #[ignore = "Integration test"] async fn test_zone_and_record_lifecycle() { let metadata = Arc::new(DnsMetadataStore::new_in_memory()); // 1. Create zone let zone_name = ZoneName::new("example.com").unwrap(); let zone = Zone::new(zone_name, "test-org", "test-project"); metadata.save_zone(&zone).await.unwrap(); // 2. Verify zone was created let loaded_zone = metadata .load_zone("test-org", "test-project", "example.com.") .await .unwrap(); assert!(loaded_zone.is_some()); assert_eq!(loaded_zone.unwrap().id, zone.id); // 3. Add A record let record_data = RecordData::a_from_str("192.168.1.100").unwrap(); let mut record = Record::new(zone.id, "www", record_data); record.ttl = Ttl::new(300).unwrap(); metadata.save_record(&record).await.unwrap(); // 4. Verify record via metadata let loaded = metadata .load_record(&zone.id, "www", RecordType::A) .await .unwrap(); assert!(loaded.is_some()); let loaded_record = loaded.unwrap(); assert_eq!(loaded_record.id, record.id); assert_eq!(loaded_record.ttl.as_secs(), 300); // 5. List records let records = metadata.list_records(&zone.id).await.unwrap(); assert_eq!(records.len(), 1); // 6. Add more records let ipv6: std::net::Ipv6Addr = "2001:db8::1".parse().unwrap(); let aaaa_data = RecordData::Aaaa { address: ipv6.octets() }; let aaaa_record = Record::new(zone.id, "www", aaaa_data); metadata.save_record(&aaaa_record).await.unwrap(); let mx_data = RecordData::Mx { preference: 10, exchange: "mail.example.com.".to_string(), }; let mx_record = Record::new(zone.id, "@", mx_data); metadata.save_record(&mx_record).await.unwrap(); let txt_data = RecordData::Txt { text: "v=spf1 include:_spf.example.com ~all".to_string(), }; let txt_record = Record::new(zone.id, "@", txt_data); metadata.save_record(&txt_record).await.unwrap(); // 7. List all records - should have 4 let all_records = metadata.list_records(&zone.id).await.unwrap(); assert_eq!(all_records.len(), 4); // 8. List records by name let www_records = metadata.list_records_by_name(&zone.id, "www").await.unwrap(); assert_eq!(www_records.len(), 2); // A + AAAA let root_records = metadata.list_records_by_name(&zone.id, "@").await.unwrap(); assert_eq!(root_records.len(), 2); // MX + TXT // 9. Cleanup - delete records metadata.delete_record(&record).await.unwrap(); metadata.delete_record(&aaaa_record).await.unwrap(); metadata.delete_record(&mx_record).await.unwrap(); metadata.delete_record(&txt_record).await.unwrap(); // 10. Verify records deleted let remaining = metadata.list_records(&zone.id).await.unwrap(); assert_eq!(remaining.len(), 0); // 11. Delete zone metadata.delete_zone(&zone).await.unwrap(); // 12. Verify zone deleted let deleted_zone = metadata .load_zone("test-org", "test-project", "example.com.") .await .unwrap(); assert!(deleted_zone.is_none()); } /// Test multi-zone scenario #[tokio::test] #[ignore = "Integration test"] async fn test_multi_zone_scenario() { let metadata = Arc::new(DnsMetadataStore::new_in_memory()); // Create multiple zones let zone1 = Zone::new( ZoneName::new("example.com").unwrap(), "org1", "project1", ); let zone2 = Zone::new( ZoneName::new("example.org").unwrap(), "org1", "project1", ); let zone3 = Zone::new( ZoneName::new("other.net").unwrap(), "org2", "project2", ); metadata.save_zone(&zone1).await.unwrap(); metadata.save_zone(&zone2).await.unwrap(); metadata.save_zone(&zone3).await.unwrap(); // Add records to each zone let a1 = Record::new( zone1.id, "www", RecordData::a_from_str("10.0.0.1").unwrap(), ); let a2 = Record::new( zone2.id, "www", RecordData::a_from_str("10.0.0.2").unwrap(), ); let a3 = Record::new( zone3.id, "www", RecordData::a_from_str("10.0.0.3").unwrap(), ); metadata.save_record(&a1).await.unwrap(); metadata.save_record(&a2).await.unwrap(); metadata.save_record(&a3).await.unwrap(); // List zones for org1 - should have 2 let org1_zones = metadata.list_zones("org1", None).await.unwrap(); assert_eq!(org1_zones.len(), 2); // List zones for org1/project1 - should have 2 let org1_p1_zones = metadata.list_zones("org1", Some("project1")).await.unwrap(); assert_eq!(org1_p1_zones.len(), 2); // List zones for org2 - should have 1 let org2_zones = metadata.list_zones("org2", None).await.unwrap(); assert_eq!(org2_zones.len(), 1); // Load zone by ID let loaded = metadata.load_zone_by_id(&zone1.id).await.unwrap(); assert!(loaded.is_some()); assert_eq!(loaded.unwrap().name.as_str(), "example.com."); // Cleanup metadata.delete_zone_records(&zone1.id).await.unwrap(); metadata.delete_zone_records(&zone2.id).await.unwrap(); metadata.delete_zone_records(&zone3.id).await.unwrap(); metadata.delete_zone(&zone1).await.unwrap(); metadata.delete_zone(&zone2).await.unwrap(); metadata.delete_zone(&zone3).await.unwrap(); } /// Test record type coverage #[tokio::test] #[ignore = "Integration test"] async fn test_record_type_coverage() { let metadata = Arc::new(DnsMetadataStore::new_in_memory()); let zone = Zone::new( ZoneName::new("types.test").unwrap(), "test-org", "test-project", ); metadata.save_zone(&zone).await.unwrap(); // A record let a = Record::new( zone.id, "a", RecordData::a_from_str("192.168.1.1").unwrap(), ); metadata.save_record(&a).await.unwrap(); // AAAA record let ipv6: std::net::Ipv6Addr = "2001:db8::1".parse().unwrap(); let aaaa = Record::new( zone.id, "aaaa", RecordData::Aaaa { address: ipv6.octets() }, ); metadata.save_record(&aaaa).await.unwrap(); // CNAME record let cname = Record::new( zone.id, "cname", RecordData::Cname { target: "target.types.test.".to_string(), }, ); metadata.save_record(&cname).await.unwrap(); // MX record let mx = Record::new( zone.id, "mx", RecordData::Mx { preference: 10, exchange: "mail.types.test.".to_string(), }, ); metadata.save_record(&mx).await.unwrap(); // TXT record let txt = Record::new( zone.id, "txt", RecordData::Txt { text: "test value".to_string(), }, ); metadata.save_record(&txt).await.unwrap(); // NS record let ns = Record::new( zone.id, "ns", RecordData::Ns { nameserver: "ns1.types.test.".to_string(), }, ); metadata.save_record(&ns).await.unwrap(); // SRV record let srv = Record::new( zone.id, "_sip._tcp", RecordData::Srv { priority: 10, weight: 20, port: 5060, target: "sip.types.test.".to_string(), }, ); metadata.save_record(&srv).await.unwrap(); // PTR record let ptr = Record::new( zone.id, "1.1.168.192.in-addr.arpa", RecordData::Ptr { target: "host.types.test.".to_string(), }, ); metadata.save_record(&ptr).await.unwrap(); // CAA record let caa = Record::new( zone.id, "caa", RecordData::Caa { flags: 0, tag: "issue".to_string(), value: "letsencrypt.org".to_string(), }, ); metadata.save_record(&caa).await.unwrap(); // Verify all records let records = metadata.list_records(&zone.id).await.unwrap(); assert_eq!(records.len(), 9); // Cleanup metadata.delete_zone_records(&zone.id).await.unwrap(); metadata.delete_zone(&zone).await.unwrap(); } /// Manual test documentation for DNS query resolution /// /// To test DNS query resolution manually: /// /// 1. Start the server: /// ``` /// cargo run -p flashdns-server /// ``` /// /// 2. Create a zone via gRPC (using grpcurl): /// ``` /// grpcurl -plaintext -d '{"name":"example.com","org_id":"test","project_id":"test"}' \ /// localhost:9053 flashdns.ZoneService/CreateZone /// ``` /// /// 3. Add an A record: /// ``` /// grpcurl -plaintext -d '{"zone_id":"","name":"www","record_type":"A","ttl":300,"data":{"a":{"address":"192.168.1.100"}}}' \ /// localhost:9053 flashdns.RecordService/CreateRecord /// ``` /// /// 4. Query via DNS: /// ``` /// dig @127.0.0.1 -p 5353 www.example.com A /// ``` /// /// Expected: Answer section should contain www.example.com with 192.168.1.100 #[tokio::test] #[ignore = "Integration test - requires DNS handler and manual verification"] async fn test_dns_query_resolution_docs() { // This test documents manual testing procedure // Actual automated DNS query testing would require: // 1. Starting DnsHandler on a test port // 2. Using a DNS client library to send queries // 3. Verifying responses // For CI, we verify the components individually: // - DnsMetadataStore (tested above) // - DnsQueryHandler logic (unit tested in handler.rs) // - Wire format (handled by trust-dns-proto) } /// Test zone listing pagination #[tokio::test] #[ignore = "Integration test"] async fn test_zone_pagination() { let metadata = Arc::new(DnsMetadataStore::new_in_memory()); let zone_service = ZoneServiceImpl::new(metadata.clone()); // Create 15 zones for i in 1..=15 { let zone_name = format!("zone{:02}.example.com", i); let zone = Zone::new( ZoneName::new(&zone_name).unwrap(), "test-org", "test-project", ); metadata.save_zone(&zone).await.unwrap(); } // Test 1: List first page with page_size=5 let request = Request::new(ListZonesRequest { org_id: "test-org".to_string(), project_id: "test-project".to_string(), name_filter: String::new(), page_size: 5, page_token: String::new(), }); let response: Response = zone_service.list_zones(request).await.unwrap(); let page1 = response.into_inner(); assert_eq!(page1.zones.len(), 5); assert!(!page1.next_page_token.is_empty(), "Should have next page token"); // Test 2: Fetch second page using next_page_token let request = Request::new(ListZonesRequest { org_id: "test-org".to_string(), project_id: "test-project".to_string(), name_filter: String::new(), page_size: 5, page_token: page1.next_page_token.clone(), }); let response: Response = zone_service.list_zones(request).await.unwrap(); let page2 = response.into_inner(); assert_eq!(page2.zones.len(), 5); assert!(!page2.next_page_token.is_empty(), "Should have next page token"); // Test 3: Fetch third page let request = Request::new(ListZonesRequest { org_id: "test-org".to_string(), project_id: "test-project".to_string(), name_filter: String::new(), page_size: 5, page_token: page2.next_page_token.clone(), }); let response: Response = zone_service.list_zones(request).await.unwrap(); let page3 = response.into_inner(); assert_eq!(page3.zones.len(), 5); assert!(page3.next_page_token.is_empty(), "Should NOT have next page token (last page)"); // Test 4: Verify zone IDs are unique across pages let all_zone_ids: Vec = page1 .zones .iter() .chain(page2.zones.iter()) .chain(page3.zones.iter()) .map(|z| z.id.clone()) .collect(); assert_eq!(all_zone_ids.len(), 15); let unique_ids: std::collections::HashSet<_> = all_zone_ids.iter().collect(); assert_eq!(unique_ids.len(), 15, "All zone IDs should be unique"); // Test 5: Default page size (page_size=0 should use default of 50) let request = Request::new(ListZonesRequest { org_id: "test-org".to_string(), project_id: "test-project".to_string(), name_filter: String::new(), page_size: 0, page_token: String::new(), }); let response: Response = zone_service.list_zones(request).await.unwrap(); let default_page = response.into_inner(); assert_eq!(default_page.zones.len(), 15, "Should return all zones with default page size"); assert!(default_page.next_page_token.is_empty()); } /// Test record listing pagination #[tokio::test] #[ignore = "Integration test"] async fn test_record_pagination() { let metadata = Arc::new(DnsMetadataStore::new_in_memory()); let record_service = RecordServiceImpl::new(metadata.clone()); // Create a zone let zone = Zone::new( ZoneName::new("example.com").unwrap(), "test-org", "test-project", ); metadata.save_zone(&zone).await.unwrap(); // Create 25 A records for i in 1..=25 { let name = format!("host{:02}", i); let address = format!("10.0.0.{}", i); let record_data = RecordData::a_from_str(&address).unwrap(); let record = Record::new(zone.id, &name, record_data); metadata.save_record(&record).await.unwrap(); } // Test 1: List first page with page_size=10 let request = Request::new(ListRecordsRequest { zone_id: zone.id.to_string(), name_filter: String::new(), type_filter: String::new(), page_size: 10, page_token: String::new(), }); let response: Response = record_service.list_records(request).await.unwrap(); let page1 = response.into_inner(); assert_eq!(page1.records.len(), 10); assert!(!page1.next_page_token.is_empty(), "Should have next page token"); // Test 2: Fetch second page let request = Request::new(ListRecordsRequest { zone_id: zone.id.to_string(), name_filter: String::new(), type_filter: String::new(), page_size: 10, page_token: page1.next_page_token.clone(), }); let response: Response = record_service.list_records(request).await.unwrap(); let page2 = response.into_inner(); assert_eq!(page2.records.len(), 10); assert!(!page2.next_page_token.is_empty(), "Should have next page token"); // Test 3: Fetch third page (partial) let request = Request::new(ListRecordsRequest { zone_id: zone.id.to_string(), name_filter: String::new(), type_filter: String::new(), page_size: 10, page_token: page2.next_page_token.clone(), }); let response: Response = record_service.list_records(request).await.unwrap(); let page3 = response.into_inner(); assert_eq!(page3.records.len(), 5, "Last page should have remaining 5 records"); assert!(page3.next_page_token.is_empty(), "Should NOT have next page token (last page)"); // Test 4: Verify all record IDs are unique let all_record_ids: Vec = page1 .records .iter() .chain(page2.records.iter()) .chain(page3.records.iter()) .map(|r| r.id.clone()) .collect(); assert_eq!(all_record_ids.len(), 25); let unique_ids: std::collections::HashSet<_> = all_record_ids.iter().collect(); assert_eq!(unique_ids.len(), 25, "All record IDs should be unique"); // Test 5: Pagination with name filter let request = Request::new(ListRecordsRequest { zone_id: zone.id.to_string(), name_filter: "host1".to_string(), // Matches host1, host10-19 type_filter: String::new(), page_size: 5, page_token: String::new(), }); let response: Response = record_service.list_records(request).await.unwrap(); let filtered_page1 = response.into_inner(); assert_eq!(filtered_page1.records.len(), 5); assert!(!filtered_page1.next_page_token.is_empty()); // Continue to second page of filtered results let request = Request::new(ListRecordsRequest { zone_id: zone.id.to_string(), name_filter: "host1".to_string(), type_filter: String::new(), page_size: 5, page_token: filtered_page1.next_page_token.clone(), }); let response: Response = record_service.list_records(request).await.unwrap(); let filtered_page2 = response.into_inner(); assert!(filtered_page2.records.len() <= 6); // host1 + host10-19 = 11 total, so 5+6 assert!(filtered_page2.next_page_token.is_empty()); // Verify all filtered records contain "host1" for record in filtered_page1.records.iter().chain(filtered_page2.records.iter()) { assert!(record.name.contains("host1"), "Filtered record should match name filter"); } }