photoncloud-monorepo/flashdns/crates/flashdns-server/tests/integration.rs
centra 3eeb303dcb feat: Batch commit for T039.S3 deployment
Includes all pending changes needed for nixos-anywhere:
- fiberlb: L7 policy, rule, certificate types
- deployer: New service for cluster management
- nix-nos: Generic network modules
- Various service updates and fixes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 04:34:51 +09:00

544 lines
17 KiB
Rust

//! 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":"<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<ListZonesResponse> = 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 = 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 = 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<String> = 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 = 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<ListRecordsResponse> = 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 = 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 = 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<String> = 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 = 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 = 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");
}
}