#[cfg(test)]
mod tests {
use crate::{
CAARecord, DnsRecord, DnsRecordType, Error, MXRecord, SRVRecord, TLSARecord, TlsaCertUsage,
TlsaMatching, TlsaSelector, providers::cloudflare::CloudflareProvider,
};
use mockito::{Matcher, Mock, ServerGuard};
use serde_json::json;
use std::time::Duration;
const ZONE_ID: &str = "abc123zoneid";
fn setup_provider(endpoint: String) -> CloudflareProvider {
CloudflareProvider::new("test_token", Some(Duration::from_secs(1)))
.unwrap()
.with_endpoint(endpoint)
}
fn mock_zone_lookup(server: &mut ServerGuard, zone_name: &str) -> Mock {
server
.mock("GET", "/zones")
.match_query(Matcher::UrlEncoded("name".into(), zone_name.into()))
.match_header("authorization", "Bearer test_token")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(format!(
r#"{{"errors":[],"success":true,"result":[{{"id":"{ZONE_ID}","name":"{zone_name}"}}]}}"#
))
.create()
}
fn mock_list(
server: &mut ServerGuard,
name: &str,
record_type: &str,
body: serde_json::Value,
) -> Mock {
server
.mock("GET", format!("/zones/{ZONE_ID}/dns_records").as_str())
.match_query(Matcher::AllOf(vec![
Matcher::UrlEncoded("name".into(), name.into()),
Matcher::UrlEncoded("type".into(), record_type.into()),
Matcher::UrlEncoded("match".into(), "all".into()),
Matcher::UrlEncoded("per_page".into(), "100".into()),
]))
.match_header("authorization", "Bearer test_token")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(serde_json::to_string(&body).unwrap())
.create()
}
fn ok_body() -> &'static str {
r#"{"errors":[],"success":true,"result":{}}"#
}
fn ok_result(items: serde_json::Value) -> serde_json::Value {
json!({"errors": [], "success": true, "result": items})
}
#[tokio::test]
async fn test_set_rrset_creates_when_owner_is_empty() {
let mut server = mockito::Server::new_async().await;
let zone = mock_zone_lookup(&mut server, "example.com");
let list = mock_list(
&mut server,
"mail.example.com",
"TLSA",
ok_result(json!([])),
);
let create_1 = server
.mock("POST", format!("/zones/{ZONE_ID}/dns_records").as_str())
.match_body(Matcher::Json(json!({
"ttl": 300,
"proxied": false,
"name": "mail.example.com",
"type": "TLSA",
"data": {
"usage": 3,
"selector": 1,
"matching_type": 1,
"certificate": "00",
},
})))
.with_status(200)
.with_body(ok_body())
.create();
let create_2 = server
.mock("POST", format!("/zones/{ZONE_ID}/dns_records").as_str())
.match_body(Matcher::Json(json!({
"ttl": 300,
"proxied": false,
"name": "mail.example.com",
"type": "TLSA",
"data": {
"usage": 2,
"selector": 1,
"matching_type": 1,
"certificate": "ff",
},
})))
.with_status(200)
.with_body(ok_body())
.create();
let provider = setup_provider(server.url());
let result = provider
.set_rrset(
"mail.example.com",
DnsRecordType::TLSA,
300,
vec![
DnsRecord::TLSA(TLSARecord {
cert_usage: TlsaCertUsage::DaneEe,
selector: TlsaSelector::Spki,
matching: TlsaMatching::Sha256,
cert_data: vec![0x00],
}),
DnsRecord::TLSA(TLSARecord {
cert_usage: TlsaCertUsage::DaneTa,
selector: TlsaSelector::Spki,
matching: TlsaMatching::Sha256,
cert_data: vec![0xff],
}),
],
"example.com",
)
.await;
assert!(result.is_ok(), "set_rrset returned: {result:?}");
zone.assert();
list.assert();
create_1.assert();
create_2.assert();
}
#[tokio::test]
async fn test_set_rrset_is_noop_when_already_matches() {
let mut server = mockito::Server::new_async().await;
let zone = mock_zone_lookup(&mut server, "example.com");
let list = mock_list(
&mut server,
"test.example.com",
"A",
ok_result(json!([
{"id": "rec-1", "type": "A", "content": "1.1.1.1"},
])),
);
let provider = setup_provider(server.url());
let result = provider
.set_rrset(
"test.example.com",
DnsRecordType::A,
300,
vec![DnsRecord::A("1.1.1.1".parse().unwrap())],
"example.com",
)
.await;
assert!(result.is_ok(), "set_rrset returned: {result:?}");
zone.assert();
list.assert();
}
#[tokio::test]
async fn test_set_rrset_deletes_extras_and_keeps_matching() {
let mut server = mockito::Server::new_async().await;
let zone = mock_zone_lookup(&mut server, "example.com");
let list = mock_list(
&mut server,
"host.example.com",
"A",
ok_result(json!([
{"id": "rec-keep", "type": "A", "content": "1.1.1.1"},
{"id": "rec-stale", "type": "A", "content": "9.9.9.9"},
])),
);
let delete_stale = server
.mock(
"DELETE",
format!("/zones/{ZONE_ID}/dns_records/rec-stale").as_str(),
)
.with_status(200)
.with_body(ok_body())
.create();
let create_new = server
.mock("POST", format!("/zones/{ZONE_ID}/dns_records").as_str())
.match_body(Matcher::Json(json!({
"ttl": 300,
"proxied": false,
"name": "host.example.com",
"type": "A",
"content": "8.8.8.8",
})))
.with_status(200)
.with_body(ok_body())
.create();
let provider = setup_provider(server.url());
let result = provider
.set_rrset(
"host.example.com",
DnsRecordType::A,
300,
vec![
DnsRecord::A("1.1.1.1".parse().unwrap()),
DnsRecord::A("8.8.8.8".parse().unwrap()),
],
"example.com",
)
.await;
assert!(result.is_ok(), "set_rrset returned: {result:?}");
zone.assert();
list.assert();
delete_stale.assert();
create_new.assert();
}
#[tokio::test]
async fn test_set_rrset_empty_records_deletes_all() {
let mut server = mockito::Server::new_async().await;
let zone = mock_zone_lookup(&mut server, "example.com");
let list = mock_list(
&mut server,
"gone.example.com",
"A",
ok_result(json!([
{"id": "rec-x", "type": "A", "content": "1.2.3.4"},
{"id": "rec-y", "type": "A", "content": "5.6.7.8"},
])),
);
let delete_x = server
.mock(
"DELETE",
format!("/zones/{ZONE_ID}/dns_records/rec-x").as_str(),
)
.with_status(200)
.with_body(ok_body())
.create();
let delete_y = server
.mock(
"DELETE",
format!("/zones/{ZONE_ID}/dns_records/rec-y").as_str(),
)
.with_status(200)
.with_body(ok_body())
.create();
let provider = setup_provider(server.url());
let result = provider
.set_rrset(
"gone.example.com",
DnsRecordType::A,
300,
vec![],
"example.com",
)
.await;
assert!(result.is_ok(), "set_rrset returned: {result:?}");
zone.assert();
list.assert();
delete_x.assert();
delete_y.assert();
}
#[tokio::test]
async fn test_add_to_rrset_skips_existing_values() {
let mut server = mockito::Server::new_async().await;
let zone = mock_zone_lookup(&mut server, "example.com");
let list = mock_list(
&mut server,
"_acme.example.com",
"TXT",
ok_result(json!([
{"id": "rec-old", "type": "TXT", "content": "\"existing\""},
])),
);
let create_new = server
.mock("POST", format!("/zones/{ZONE_ID}/dns_records").as_str())
.match_body(Matcher::Json(json!({
"ttl": 60,
"proxied": false,
"name": "_acme.example.com",
"type": "TXT",
"content": "\"new-token\"",
})))
.with_status(200)
.with_body(ok_body())
.create();
let provider = setup_provider(server.url());
let result = provider
.add_to_rrset(
"_acme.example.com",
DnsRecordType::TXT,
60,
vec![
DnsRecord::TXT("existing".to_string()),
DnsRecord::TXT("new-token".to_string()),
],
"example.com",
)
.await;
assert!(result.is_ok(), "add_to_rrset returned: {result:?}");
zone.assert();
list.assert();
create_new.assert();
}
#[tokio::test]
async fn test_remove_from_rrset_deletes_only_matching_values() {
let mut server = mockito::Server::new_async().await;
let zone = mock_zone_lookup(&mut server, "example.com");
let list = mock_list(
&mut server,
"_acme.example.com",
"TXT",
ok_result(json!([
{"id": "rec-keep", "type": "TXT", "content": "\"keep-me\""},
{"id": "rec-drop", "type": "TXT", "content": "\"drop-me\""},
])),
);
let delete = server
.mock(
"DELETE",
format!("/zones/{ZONE_ID}/dns_records/rec-drop").as_str(),
)
.with_status(200)
.with_body(ok_body())
.create();
let provider = setup_provider(server.url());
let result = provider
.remove_from_rrset(
"_acme.example.com",
DnsRecordType::TXT,
vec![DnsRecord::TXT("drop-me".to_string())],
"example.com",
)
.await;
assert!(result.is_ok(), "remove_from_rrset returned: {result:?}");
zone.assert();
list.assert();
delete.assert();
}
#[tokio::test]
async fn test_set_rrset_replaces_two_tlsa_at_same_owner() {
let mut server = mockito::Server::new_async().await;
let zone = mock_zone_lookup(&mut server, "example.com");
let list = mock_list(
&mut server,
"_25._tcp.mail.example.com",
"TLSA",
ok_result(json!([
{"id": "old-ee", "type": "TLSA", "data": {
"usage": 3, "selector": 1, "matching_type": 1, "certificate": "aa"
}},
{"id": "old-ta", "type": "TLSA", "data": {
"usage": 2, "selector": 1, "matching_type": 1, "certificate": "bb"
}},
])),
);
let delete_ee = server
.mock(
"DELETE",
format!("/zones/{ZONE_ID}/dns_records/old-ee").as_str(),
)
.with_status(200)
.with_body(ok_body())
.create();
let delete_ta = server
.mock(
"DELETE",
format!("/zones/{ZONE_ID}/dns_records/old-ta").as_str(),
)
.with_status(200)
.with_body(ok_body())
.create();
let create_ee = server
.mock("POST", format!("/zones/{ZONE_ID}/dns_records").as_str())
.match_body(Matcher::PartialJson(json!({
"data": {"usage": 3, "certificate": "cc"}
})))
.with_status(200)
.with_body(ok_body())
.create();
let create_ta = server
.mock("POST", format!("/zones/{ZONE_ID}/dns_records").as_str())
.match_body(Matcher::PartialJson(json!({
"data": {"usage": 2, "certificate": "dd"}
})))
.with_status(200)
.with_body(ok_body())
.create();
let provider = setup_provider(server.url());
let result = provider
.set_rrset(
"_25._tcp.mail.example.com",
DnsRecordType::TLSA,
300,
vec![
DnsRecord::TLSA(TLSARecord {
cert_usage: TlsaCertUsage::DaneEe,
selector: TlsaSelector::Spki,
matching: TlsaMatching::Sha256,
cert_data: vec![0xcc],
}),
DnsRecord::TLSA(TLSARecord {
cert_usage: TlsaCertUsage::DaneTa,
selector: TlsaSelector::Spki,
matching: TlsaMatching::Sha256,
cert_data: vec![0xdd],
}),
],
"example.com",
)
.await;
assert!(result.is_ok(), "set_rrset returned: {result:?}");
zone.assert();
list.assert();
delete_ee.assert();
delete_ta.assert();
create_ee.assert();
create_ta.assert();
}
#[tokio::test]
async fn test_set_rrset_records_must_match_declared_type() {
let mut server = mockito::Server::new_async().await;
let _zone = mock_zone_lookup(&mut server, "example.com");
let provider = setup_provider(server.url());
let result = provider
.set_rrset(
"test.example.com",
DnsRecordType::A,
300,
vec![DnsRecord::TXT("not-an-A".to_string())],
"example.com",
)
.await;
assert!(matches!(result, Err(Error::Api(_))), "got {result:?}");
}
#[tokio::test]
async fn test_obtain_zone_id_walks_up_to_parent_zone() {
let mut server = mockito::Server::new_async().await;
let zone_miss = server
.mock("GET", "/zones")
.match_query(Matcher::UrlEncoded("name".into(), "sub.example.com".into()))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"errors":[],"success":true,"result":[]}"#)
.create();
let zone_hit = mock_zone_lookup(&mut server, "example.com");
let list = mock_list(
&mut server,
"host.sub.example.com",
"A",
ok_result(json!([])),
);
let create = server
.mock("POST", format!("/zones/{ZONE_ID}/dns_records").as_str())
.with_status(200)
.with_body(ok_body())
.create();
let provider = setup_provider(server.url());
let result = provider
.set_rrset(
"host.sub.example.com",
DnsRecordType::A,
3600,
vec![DnsRecord::A("1.2.3.4".parse().unwrap())],
"sub.example.com",
)
.await;
assert!(result.is_ok(), "set_rrset returned: {result:?}");
zone_miss.assert();
zone_hit.assert();
list.assert();
create.assert();
}
#[tokio::test]
async fn test_unauthorized_response_maps_to_error_unauthorized() {
let mut server = mockito::Server::new_async().await;
let unauthorized = server
.mock("GET", "/zones")
.match_query(Matcher::UrlEncoded("name".into(), "example.com".into()))
.with_status(401)
.with_body(
r#"{"errors":[{"code":10000,"message":"Unauthorized"}],"success":false,"result":[]}"#,
)
.create();
let provider = setup_provider(server.url());
let result = provider
.set_rrset(
"test.example.com",
DnsRecordType::A,
3600,
vec![DnsRecord::A("1.1.1.1".parse().unwrap())],
"example.com",
)
.await;
assert!(
matches!(result, Err(Error::Unauthorized)),
"expected Unauthorized, got {result:?}"
);
unauthorized.assert();
}
#[tokio::test]
async fn test_api_success_false_surfaces_as_api_error() {
let mut server = mockito::Server::new_async().await;
let failure = server
.mock("GET", "/zones")
.match_query(Matcher::Any)
.with_status(200)
.with_body(
r#"{"errors":[{"code":1003,"message":"Invalid zone identifier"}],"success":false,"result":[]}"#,
)
.create();
let provider = setup_provider(server.url());
let result = provider
.set_rrset(
"test.example.com",
DnsRecordType::A,
3600,
vec![DnsRecord::A("1.1.1.1".parse().unwrap())],
"example.com",
)
.await;
assert!(
matches!(result, Err(Error::Api(_))),
"expected Error::Api, got {result:?}"
);
failure.assert();
}
#[tokio::test]
async fn test_mx_create_sends_priority_via_set_rrset() {
let mut server = mockito::Server::new_async().await;
let zone = mock_zone_lookup(&mut server, "example.com");
let list = mock_list(&mut server, "example.com", "MX", ok_result(json!([])));
let create = server
.mock("POST", format!("/zones/{ZONE_ID}/dns_records").as_str())
.match_body(Matcher::Json(json!({
"ttl": 3600,
"priority": 10,
"proxied": false,
"name": "example.com",
"type": "MX",
"content": "mail.example.com",
})))
.with_status(200)
.with_body(ok_body())
.create();
let provider = setup_provider(server.url());
let result = provider
.set_rrset(
"example.com",
DnsRecordType::MX,
3600,
vec![DnsRecord::MX(MXRecord {
exchange: "mail.example.com".to_string(),
priority: 10,
})],
"example.com",
)
.await;
assert!(result.is_ok(), "set_rrset returned: {result:?}");
zone.assert();
list.assert();
create.assert();
}
#[tokio::test]
async fn test_caa_via_set_rrset_uses_data_struct() {
let mut server = mockito::Server::new_async().await;
let zone = mock_zone_lookup(&mut server, "example.com");
let list = mock_list(&mut server, "example.com", "CAA", ok_result(json!([])));
let create = server
.mock("POST", format!("/zones/{ZONE_ID}/dns_records").as_str())
.match_body(Matcher::Json(json!({
"ttl": 3600,
"proxied": false,
"name": "example.com",
"type": "CAA",
"data": {
"flags": 0,
"tag": "issue",
"value": "letsencrypt.org",
},
})))
.with_status(200)
.with_body(ok_body())
.create();
let provider = setup_provider(server.url());
let result = provider
.set_rrset(
"example.com",
DnsRecordType::CAA,
3600,
vec![DnsRecord::CAA(CAARecord::Issue {
issuer_critical: false,
name: Some("letsencrypt.org".to_string()),
options: vec![],
})],
"example.com",
)
.await;
assert!(result.is_ok(), "set_rrset returned: {result:?}");
zone.assert();
list.assert();
create.assert();
}
#[tokio::test]
async fn test_srv_via_set_rrset_uses_data_struct() {
let mut server = mockito::Server::new_async().await;
let zone = mock_zone_lookup(&mut server, "example.com");
let list = mock_list(
&mut server,
"_imaps._tcp.example.com",
"SRV",
ok_result(json!([])),
);
let create = server
.mock("POST", format!("/zones/{ZONE_ID}/dns_records").as_str())
.match_body(Matcher::Json(json!({
"ttl": 3600,
"proxied": false,
"name": "_imaps._tcp.example.com",
"type": "SRV",
"data": {
"priority": 10,
"weight": 5,
"port": 993,
"target": "mail.example.com",
},
})))
.with_status(200)
.with_body(ok_body())
.create();
let provider = setup_provider(server.url());
let result = provider
.set_rrset(
"_imaps._tcp.example.com",
DnsRecordType::SRV,
3600,
vec![DnsRecord::SRV(SRVRecord {
priority: 10,
weight: 5,
port: 993,
target: "mail.example.com".to_string(),
})],
"example.com",
)
.await;
assert!(result.is_ok(), "set_rrset returned: {result:?}");
zone.assert();
list.assert();
create.assert();
}
#[tokio::test]
async fn test_list_filter_by_type_protects_other_types_at_same_owner() {
let mut server = mockito::Server::new_async().await;
let zone = mock_zone_lookup(&mut server, "example.com");
let list = server
.mock("GET", format!("/zones/{ZONE_ID}/dns_records").as_str())
.match_query(Matcher::AllOf(vec![
Matcher::UrlEncoded("name".into(), "shared.example.com".into()),
Matcher::UrlEncoded("type".into(), "A".into()),
Matcher::UrlEncoded("match".into(), "all".into()),
Matcher::UrlEncoded("per_page".into(), "100".into()),
]))
.with_status(200)
.with_body(serde_json::to_string(&ok_result(json!([]))).unwrap())
.expect(1)
.create();
let _txt_get_must_not_fire = server
.mock("GET", format!("/zones/{ZONE_ID}/dns_records").as_str())
.match_query(Matcher::UrlEncoded("type".into(), "TXT".into()))
.with_status(500)
.expect(0)
.create();
let create = server
.mock("POST", format!("/zones/{ZONE_ID}/dns_records").as_str())
.match_body(Matcher::PartialJson(json!({
"name": "shared.example.com",
"type": "A",
"content": "1.1.1.1",
})))
.with_status(200)
.with_body(ok_body())
.create();
let provider = setup_provider(server.url());
let result = provider
.set_rrset(
"shared.example.com",
DnsRecordType::A,
300,
vec![DnsRecord::A("1.1.1.1".parse().unwrap())],
"example.com",
)
.await;
assert!(result.is_ok(), "set_rrset returned: {result:?}");
zone.assert();
list.assert();
create.assert();
}
#[tokio::test]
async fn test_txt_with_embedded_quotes_roundtrips_correctly() {
let mut server = mockito::Server::new_async().await;
let zone = mock_zone_lookup(&mut server, "example.com");
let list = mock_list(
&mut server,
"test.example.com",
"TXT",
ok_result(json!([
{"id": "rec-1", "type": "TXT", "content": "\"hello \\\"world\\\"\""},
])),
);
let _no_post = server
.mock("POST", format!("/zones/{ZONE_ID}/dns_records").as_str())
.expect(0)
.create();
let provider = setup_provider(server.url());
let result = provider
.set_rrset(
"test.example.com",
DnsRecordType::TXT,
300,
vec![DnsRecord::TXT(r#"hello "world""#.to_string())],
"example.com",
)
.await;
assert!(result.is_ok(), "set_rrset returned: {result:?}");
zone.assert();
list.assert();
}
#[tokio::test]
async fn test_add_to_rrset_is_full_noop_when_everything_present() {
let mut server = mockito::Server::new_async().await;
let zone = mock_zone_lookup(&mut server, "example.com");
let list = mock_list(
&mut server,
"test.example.com",
"A",
ok_result(json!([
{"id": "rec-1", "type": "A", "content": "1.1.1.1"},
{"id": "rec-2", "type": "A", "content": "8.8.8.8"},
])),
);
let _no_post = server
.mock("POST", format!("/zones/{ZONE_ID}/dns_records").as_str())
.expect(0)
.create();
let provider = setup_provider(server.url());
let result = provider
.add_to_rrset(
"test.example.com",
DnsRecordType::A,
300,
vec![
DnsRecord::A("1.1.1.1".parse().unwrap()),
DnsRecord::A("8.8.8.8".parse().unwrap()),
],
"example.com",
)
.await;
assert!(result.is_ok(), "add_to_rrset returned: {result:?}");
zone.assert();
list.assert();
}
#[tokio::test]
async fn test_add_to_rrset_creates_rrset_from_empty_owner() {
let mut server = mockito::Server::new_async().await;
let zone = mock_zone_lookup(&mut server, "example.com");
let list = mock_list(&mut server, "fresh.example.com", "A", ok_result(json!([])));
let create = server
.mock("POST", format!("/zones/{ZONE_ID}/dns_records").as_str())
.match_body(Matcher::PartialJson(json!({
"name": "fresh.example.com",
"type": "A",
"content": "9.9.9.9",
})))
.with_status(200)
.with_body(ok_body())
.create();
let provider = setup_provider(server.url());
let result = provider
.add_to_rrset(
"fresh.example.com",
DnsRecordType::A,
300,
vec![DnsRecord::A("9.9.9.9".parse().unwrap())],
"example.com",
)
.await;
assert!(result.is_ok(), "add_to_rrset returned: {result:?}");
zone.assert();
list.assert();
create.assert();
}
#[tokio::test]
async fn test_remove_from_rrset_noop_when_values_absent() {
let mut server = mockito::Server::new_async().await;
let zone = mock_zone_lookup(&mut server, "example.com");
let list = mock_list(
&mut server,
"test.example.com",
"A",
ok_result(json!([
{"id": "rec-1", "type": "A", "content": "1.1.1.1"},
])),
);
let _no_delete = server
.mock(
"DELETE",
Matcher::Regex(format!("^/zones/{ZONE_ID}/dns_records/")),
)
.expect(0)
.create();
let provider = setup_provider(server.url());
let result = provider
.remove_from_rrset(
"test.example.com",
DnsRecordType::A,
vec![DnsRecord::A("9.9.9.9".parse().unwrap())],
"example.com",
)
.await;
assert!(result.is_ok(), "remove_from_rrset returned: {result:?}");
zone.assert();
list.assert();
}
#[tokio::test]
async fn test_remove_from_rrset_empty_input_is_early_return() {
let mut server = mockito::Server::new_async().await;
let _no_call = server.mock("GET", Matcher::Any).expect(0).create();
let _no_delete = server.mock("DELETE", Matcher::Any).expect(0).create();
let provider = setup_provider(server.url());
let result = provider
.remove_from_rrset("test.example.com", DnsRecordType::A, vec![], "example.com")
.await;
assert!(result.is_ok(), "remove_from_rrset returned: {result:?}");
}
#[tokio::test]
async fn test_add_to_rrset_empty_input_is_early_return() {
let mut server = mockito::Server::new_async().await;
let _no_call = server.mock("GET", Matcher::Any).expect(0).create();
let provider = setup_provider(server.url());
let result = provider
.add_to_rrset(
"test.example.com",
DnsRecordType::A,
300,
vec![],
"example.com",
)
.await;
assert!(result.is_ok(), "add_to_rrset returned: {result:?}");
}
#[tokio::test]
async fn test_list_response_tolerates_extra_cloudflare_fields() {
let mut server = mockito::Server::new_async().await;
let zone = mock_zone_lookup(&mut server, "example.com");
let list = mock_list(
&mut server,
"test.example.com",
"A",
ok_result(json!([
{
"id": "rec-1",
"name": "test.example.com",
"type": "A",
"content": "1.1.1.1",
"ttl": 3600,
"proxied": false,
"comment": "user added",
"tags": ["env:prod"],
"meta": {"auto_added": false, "managed_by_apps": false},
"created_on": "2024-01-01T00:00:00Z",
"modified_on": "2024-01-01T00:00:00Z",
"zone_id": ZONE_ID,
"zone_name": "example.com",
"locked": false,
},
])),
);
let _no_mutation = server
.mock("POST", format!("/zones/{ZONE_ID}/dns_records").as_str())
.expect(0)
.create();
let provider = setup_provider(server.url());
let result = provider
.set_rrset(
"test.example.com",
DnsRecordType::A,
300,
vec![DnsRecord::A("1.1.1.1".parse().unwrap())],
"example.com",
)
.await;
assert!(result.is_ok(), "set_rrset returned: {result:?}");
zone.assert();
list.assert();
}
#[tokio::test]
async fn test_set_rrset_replaces_then_creates_when_completely_different() {
let mut server = mockito::Server::new_async().await;
let zone = mock_zone_lookup(&mut server, "example.com");
let list = mock_list(
&mut server,
"swap.example.com",
"A",
ok_result(json!([
{"id": "old-1", "type": "A", "content": "1.1.1.1"},
{"id": "old-2", "type": "A", "content": "1.1.1.2"},
])),
);
let del1 = server
.mock(
"DELETE",
format!("/zones/{ZONE_ID}/dns_records/old-1").as_str(),
)
.with_status(200)
.with_body(ok_body())
.create();
let del2 = server
.mock(
"DELETE",
format!("/zones/{ZONE_ID}/dns_records/old-2").as_str(),
)
.with_status(200)
.with_body(ok_body())
.create();
let create1 = server
.mock("POST", format!("/zones/{ZONE_ID}/dns_records").as_str())
.match_body(Matcher::PartialJson(json!({"content": "2.2.2.1"})))
.with_status(200)
.with_body(ok_body())
.create();
let create2 = server
.mock("POST", format!("/zones/{ZONE_ID}/dns_records").as_str())
.match_body(Matcher::PartialJson(json!({"content": "2.2.2.2"})))
.with_status(200)
.with_body(ok_body())
.create();
let provider = setup_provider(server.url());
let result = provider
.set_rrset(
"swap.example.com",
DnsRecordType::A,
300,
vec![
DnsRecord::A("2.2.2.1".parse().unwrap()),
DnsRecord::A("2.2.2.2".parse().unwrap()),
],
"example.com",
)
.await;
assert!(result.is_ok(), "set_rrset returned: {result:?}");
zone.assert();
list.assert();
del1.assert();
del2.assert();
create1.assert();
create2.assert();
}
#[tokio::test]
async fn test_set_rrset_apex_name_publishes_with_zone_root() {
let mut server = mockito::Server::new_async().await;
let zone = mock_zone_lookup(&mut server, "example.com");
let list = mock_list(&mut server, "example.com", "MX", ok_result(json!([])));
let create = server
.mock("POST", format!("/zones/{ZONE_ID}/dns_records").as_str())
.match_body(Matcher::PartialJson(json!({
"name": "example.com",
"type": "MX",
"content": "mail.example.com",
"priority": 10,
})))
.with_status(200)
.with_body(ok_body())
.create();
let provider = setup_provider(server.url());
let result = provider
.set_rrset(
"example.com",
DnsRecordType::MX,
3600,
vec![DnsRecord::MX(MXRecord {
exchange: "mail.example.com".to_string(),
priority: 10,
})],
"example.com",
)
.await;
assert!(result.is_ok(), "set_rrset returned: {result:?}");
zone.assert();
list.assert();
create.assert();
}
#[tokio::test]
async fn test_set_rrset_mx_with_different_priorities() {
let mut server = mockito::Server::new_async().await;
let zone = mock_zone_lookup(&mut server, "example.com");
let list = mock_list(&mut server, "example.com", "MX", ok_result(json!([])));
let primary = server
.mock("POST", format!("/zones/{ZONE_ID}/dns_records").as_str())
.match_body(Matcher::PartialJson(json!({
"priority": 10, "content": "mx1.example.com"
})))
.with_status(200)
.with_body(ok_body())
.create();
let backup = server
.mock("POST", format!("/zones/{ZONE_ID}/dns_records").as_str())
.match_body(Matcher::PartialJson(json!({
"priority": 20, "content": "mx2.example.com"
})))
.with_status(200)
.with_body(ok_body())
.create();
let provider = setup_provider(server.url());
let result = provider
.set_rrset(
"example.com",
DnsRecordType::MX,
3600,
vec![
DnsRecord::MX(MXRecord {
exchange: "mx1.example.com".to_string(),
priority: 10,
}),
DnsRecord::MX(MXRecord {
exchange: "mx2.example.com".to_string(),
priority: 20,
}),
],
"example.com",
)
.await;
assert!(result.is_ok(), "set_rrset returned: {result:?}");
zone.assert();
list.assert();
primary.assert();
backup.assert();
}
#[tokio::test]
async fn test_set_rrset_long_txt_dkim_key_sent_as_chunked_strings() {
let mut server = mockito::Server::new_async().await;
let zone = mock_zone_lookup(&mut server, "example.com");
let list = mock_list(
&mut server,
"selector._domainkey.example.com",
"TXT",
ok_result(json!([])),
);
let long_value: String = "v=DKIM1;k=rsa;p=".to_string() + &"A".repeat(380);
let (head, tail) = long_value.split_at(255);
let expected_sent = format!("\"{head}\" \"{tail}\"");
let create = server
.mock("POST", format!("/zones/{ZONE_ID}/dns_records").as_str())
.match_body(Matcher::PartialJson(json!({
"type": "TXT",
"content": expected_sent,
})))
.with_status(200)
.with_body(ok_body())
.create();
let provider = setup_provider(server.url());
let result = provider
.set_rrset(
"selector._domainkey.example.com",
DnsRecordType::TXT,
300,
vec![DnsRecord::TXT(long_value)],
"example.com",
)
.await;
assert!(result.is_ok(), "set_rrset returned: {result:?}");
zone.assert();
list.assert();
create.assert();
}
#[tokio::test]
#[ignore = "Requires CLOUDFLARE_API_TOKEN and CLOUDFLARE_ORIGIN"]
async fn integration_test() {
use crate::providers::cloudflare::DnsContent;
let token = std::env::var("CLOUDFLARE_API_TOKEN").unwrap_or_default();
let origin = std::env::var("CLOUDFLARE_ORIGIN").unwrap_or_default();
assert!(!token.is_empty(), "Set CLOUDFLARE_API_TOKEN");
assert!(
!origin.is_empty(),
"Set CLOUDFLARE_ORIGIN (e.g. example.com)"
);
let run_id = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let label = |kind: &str| format!("dnsupdate-poc-{run_id}-{kind}.{origin}");
let srv = |service: &str, kind: &str| {
format!("_{service}._tcp.dnsupdate-poc-{run_id}-{kind}.{origin}")
};
let updater = CloudflareProvider::new(&token, Some(Duration::from_secs(30))).unwrap();
let assert_contents = |label: String,
record_type: DnsRecordType,
expected: Vec<DnsContent>| {
let updater = updater.clone();
let origin = origin.clone();
async move {
let mut got = updater
.list_contents_for_tests(label.as_str(), record_type, origin.as_str())
.await
.unwrap_or_else(|err| panic!("list {label} failed: {err}"));
let mut want = expected;
let normalize = |v: &mut Vec<DnsContent>| {
v.sort_by_key(|c| serde_json::to_string(c).unwrap_or_default());
};
normalize(&mut got);
normalize(&mut want);
assert_eq!(
got, want,
"RRSet at {label}/{record_type:?} mismatch:\n want: {want:?}\n got: {got:?}"
);
}
};
let a_name = label("a");
let a1 = DnsRecord::A([10, 11, 12, 1].into());
let a2 = DnsRecord::A([10, 11, 12, 2].into());
let a3 = DnsRecord::A([10, 11, 12, 3].into());
eprintln!("[phase 1.1] set_rrset from empty to two A records");
updater
.set_rrset(
a_name.as_str(),
DnsRecordType::A,
300,
vec![a1.clone(), a2.clone()],
origin.as_str(),
)
.await
.expect("set_rrset initial A");
assert_contents(
a_name.clone(),
DnsRecordType::A,
vec![
DnsContent::A {
content: [10, 11, 12, 1].into(),
},
DnsContent::A {
content: [10, 11, 12, 2].into(),
},
],
)
.await;
eprintln!("[phase 1.2] set_rrset with same values must be a no-op");
updater
.set_rrset(
a_name.as_str(),
DnsRecordType::A,
300,
vec![a1.clone(), a2.clone()],
origin.as_str(),
)
.await
.expect("set_rrset idempotent A");
assert_contents(
a_name.clone(),
DnsRecordType::A,
vec![
DnsContent::A {
content: [10, 11, 12, 1].into(),
},
DnsContent::A {
content: [10, 11, 12, 2].into(),
},
],
)
.await;
eprintln!(
"[phase 1.3] set_rrset to a partially-overlapping set: drops a2, keeps a1, adds a3"
);
updater
.set_rrset(
a_name.as_str(),
DnsRecordType::A,
300,
vec![a1.clone(), a3.clone()],
origin.as_str(),
)
.await
.expect("set_rrset partial-overlap A");
assert_contents(
a_name.clone(),
DnsRecordType::A,
vec![
DnsContent::A {
content: [10, 11, 12, 1].into(),
},
DnsContent::A {
content: [10, 11, 12, 3].into(),
},
],
)
.await;
eprintln!("[phase 1.4] add_to_rrset adds a2 alongside existing");
updater
.add_to_rrset(
a_name.as_str(),
DnsRecordType::A,
300,
vec![a2.clone()],
origin.as_str(),
)
.await
.expect("add_to_rrset A");
assert_contents(
a_name.clone(),
DnsRecordType::A,
vec![
DnsContent::A {
content: [10, 11, 12, 1].into(),
},
DnsContent::A {
content: [10, 11, 12, 2].into(),
},
DnsContent::A {
content: [10, 11, 12, 3].into(),
},
],
)
.await;
eprintln!("[phase 1.5] add_to_rrset with already-present value is a no-op");
updater
.add_to_rrset(
a_name.as_str(),
DnsRecordType::A,
300,
vec![a1.clone()],
origin.as_str(),
)
.await
.expect("add_to_rrset idempotent A");
assert_contents(
a_name.clone(),
DnsRecordType::A,
vec![
DnsContent::A {
content: [10, 11, 12, 1].into(),
},
DnsContent::A {
content: [10, 11, 12, 2].into(),
},
DnsContent::A {
content: [10, 11, 12, 3].into(),
},
],
)
.await;
eprintln!("[phase 1.6] remove_from_rrset removes only a2");
updater
.remove_from_rrset(
a_name.as_str(),
DnsRecordType::A,
vec![a2.clone()],
origin.as_str(),
)
.await
.expect("remove_from_rrset A");
assert_contents(
a_name.clone(),
DnsRecordType::A,
vec![
DnsContent::A {
content: [10, 11, 12, 1].into(),
},
DnsContent::A {
content: [10, 11, 12, 3].into(),
},
],
)
.await;
eprintln!("[phase 1.7] remove_from_rrset on absent value is a no-op");
updater
.remove_from_rrset(
a_name.as_str(),
DnsRecordType::A,
vec![DnsRecord::A([10, 11, 12, 99].into())],
origin.as_str(),
)
.await
.expect("remove_from_rrset absent A");
assert_contents(
a_name.clone(),
DnsRecordType::A,
vec![
DnsContent::A {
content: [10, 11, 12, 1].into(),
},
DnsContent::A {
content: [10, 11, 12, 3].into(),
},
],
)
.await;
eprintln!("[phase 1.8] set_rrset(empty) wipes the entire RRSet");
updater
.set_rrset(
a_name.as_str(),
DnsRecordType::A,
300,
vec![],
origin.as_str(),
)
.await
.expect("set_rrset empty A");
assert_contents(a_name.clone(), DnsRecordType::A, vec![]).await;
let mixed_name = label("mixed");
let txt_marker = "do-not-delete-me";
eprintln!(
"[phase 2.1] seed a TXT record at {mixed_name} that simulates a user-managed record"
);
updater
.set_rrset(
mixed_name.as_str(),
DnsRecordType::TXT,
300,
vec![DnsRecord::TXT(txt_marker.to_string())],
origin.as_str(),
)
.await
.expect("seed TXT");
eprintln!("[phase 2.2] set_rrset for A at the same owner");
updater
.set_rrset(
mixed_name.as_str(),
DnsRecordType::A,
300,
vec![DnsRecord::A([10, 12, 0, 1].into())],
origin.as_str(),
)
.await
.expect("set_rrset A under mixed owner");
eprintln!("[phase 2.3] TXT at {mixed_name} must survive untouched");
assert_contents(
mixed_name.clone(),
DnsRecordType::TXT,
vec![DnsContent::TXT {
content: format!("\"{txt_marker}\""),
}],
)
.await;
assert_contents(
mixed_name.clone(),
DnsRecordType::A,
vec![DnsContent::A {
content: [10, 12, 0, 1].into(),
}],
)
.await;
eprintln!("[phase 2.4] cleanup TXT and A at mixed owner");
updater
.set_rrset(
mixed_name.as_str(),
DnsRecordType::A,
300,
vec![],
origin.as_str(),
)
.await
.expect("cleanup A");
updater
.set_rrset(
mixed_name.as_str(),
DnsRecordType::TXT,
300,
vec![],
origin.as_str(),
)
.await
.expect("cleanup TXT");
let txt_name = label("txt");
let txt_value = r#"v=verify;token="abc;def""#;
eprintln!("[phase 3.1] set TXT with embedded quotes; rerun must be a no-op");
updater
.set_rrset(
txt_name.as_str(),
DnsRecordType::TXT,
300,
vec![DnsRecord::TXT(txt_value.to_string())],
origin.as_str(),
)
.await
.expect("set TXT with quotes");
updater
.set_rrset(
txt_name.as_str(),
DnsRecordType::TXT,
300,
vec![DnsRecord::TXT(txt_value.to_string())],
origin.as_str(),
)
.await
.expect("set TXT with quotes (idempotent)");
let listed = updater
.list_contents_for_tests(txt_name.as_str(), DnsRecordType::TXT, origin.as_str())
.await
.expect("list TXT");
assert_eq!(
listed.len(),
1,
"expected 1 TXT record after idempotent reruns, got {}",
listed.len()
);
if let DnsContent::TXT { content } = &listed[0] {
assert!(
content.contains(r#"abc;def"#),
"TXT content should preserve embedded chars: got {content}"
);
} else {
panic!("expected TXT content, got {:?}", listed[0]);
}
updater
.set_rrset(
txt_name.as_str(),
DnsRecordType::TXT,
300,
vec![],
origin.as_str(),
)
.await
.expect("cleanup TXT");
let tlsa_name = srv("25", "tlsa");
let leaf = vec![0xaa; 32];
let intermediate = vec![0xbb; 32];
let leaf_renewed = vec![0xcc; 32];
let intermediate_renewed = vec![0xdd; 32];
eprintln!("[phase 4.1] publish two TLSA records at the same owner in one call");
updater
.set_rrset(
tlsa_name.as_str(),
DnsRecordType::TLSA,
300,
vec![
DnsRecord::TLSA(TLSARecord {
cert_usage: TlsaCertUsage::DaneEe,
selector: TlsaSelector::Spki,
matching: TlsaMatching::Sha256,
cert_data: leaf.clone(),
}),
DnsRecord::TLSA(TLSARecord {
cert_usage: TlsaCertUsage::DaneTa,
selector: TlsaSelector::Spki,
matching: TlsaMatching::Sha256,
cert_data: intermediate.clone(),
}),
],
origin.as_str(),
)
.await
.expect("set_rrset two TLSA");
let listed = updater
.list_contents_for_tests(tlsa_name.as_str(), DnsRecordType::TLSA, origin.as_str())
.await
.expect("list TLSA");
assert_eq!(
listed.len(),
2,
"expected 2 TLSA records, got {}",
listed.len()
);
eprintln!(
"[phase 4.2] cert renewal: same owner gets two new TLSA hashes; old ones must be gone"
);
updater
.set_rrset(
tlsa_name.as_str(),
DnsRecordType::TLSA,
300,
vec![
DnsRecord::TLSA(TLSARecord {
cert_usage: TlsaCertUsage::DaneEe,
selector: TlsaSelector::Spki,
matching: TlsaMatching::Sha256,
cert_data: leaf_renewed.clone(),
}),
DnsRecord::TLSA(TLSARecord {
cert_usage: TlsaCertUsage::DaneTa,
selector: TlsaSelector::Spki,
matching: TlsaMatching::Sha256,
cert_data: intermediate_renewed.clone(),
}),
],
origin.as_str(),
)
.await
.expect("set_rrset two TLSA renewed");
let listed = updater
.list_contents_for_tests(tlsa_name.as_str(), DnsRecordType::TLSA, origin.as_str())
.await
.expect("list TLSA after renewal");
assert_eq!(
listed.len(),
2,
"after renewal expected 2 TLSA, got {}",
listed.len()
);
for content in &listed {
if let DnsContent::TLSA { data } = content {
assert!(
data.certificate == "cc".repeat(32) || data.certificate == "dd".repeat(32),
"TLSA cert data should be the renewed hashes, got {}",
data.certificate
);
} else {
panic!("expected TLSA, got {content:?}");
}
}
updater
.set_rrset(
tlsa_name.as_str(),
DnsRecordType::TLSA,
300,
vec![],
origin.as_str(),
)
.await
.expect("cleanup TLSA");
let mx_name = label("mx");
eprintln!("[phase 5.1] set two MX records with different priorities");
updater
.set_rrset(
mx_name.as_str(),
DnsRecordType::MX,
300,
vec![
DnsRecord::MX(MXRecord {
exchange: format!("mx1.{}.", origin),
priority: 10,
}),
DnsRecord::MX(MXRecord {
exchange: format!("mx2.{}.", origin),
priority: 20,
}),
],
origin.as_str(),
)
.await
.expect("set_rrset two MX");
let listed = updater
.list_contents_for_tests(mx_name.as_str(), DnsRecordType::MX, origin.as_str())
.await
.expect("list MX");
assert_eq!(listed.len(), 2, "expected 2 MX, got {}", listed.len());
updater
.set_rrset(
mx_name.as_str(),
DnsRecordType::MX,
300,
vec![],
origin.as_str(),
)
.await
.expect("cleanup MX");
let srv_name = srv("imaps", "srv");
eprintln!("[phase 6.1] set SRV at {srv_name}");
updater
.set_rrset(
srv_name.as_str(),
DnsRecordType::SRV,
300,
vec![DnsRecord::SRV(SRVRecord {
priority: 10,
weight: 5,
port: 993,
target: format!("mail.{}.", origin),
})],
origin.as_str(),
)
.await
.expect("set_rrset SRV");
let listed = updater
.list_contents_for_tests(srv_name.as_str(), DnsRecordType::SRV, origin.as_str())
.await
.expect("list SRV");
assert_eq!(listed.len(), 1, "expected 1 SRV, got {}", listed.len());
updater
.set_rrset(
srv_name.as_str(),
DnsRecordType::SRV,
300,
vec![],
origin.as_str(),
)
.await
.expect("cleanup SRV");
let dkim_name = format!("selector{run_id}._domainkey.{origin}");
let dkim_value = format!("v=DKIM1;k=rsa;p={}", "A".repeat(390));
eprintln!(
"[phase 7.1] publish long TXT (DKIM-shaped, {} bytes)",
dkim_value.len()
);
updater
.set_rrset(
dkim_name.as_str(),
DnsRecordType::TXT,
300,
vec![DnsRecord::TXT(dkim_value.clone())],
origin.as_str(),
)
.await
.expect("set_rrset long TXT");
eprintln!("[phase 7.2] rerun must be a no-op (long TXT comparison roundtrips)");
updater
.set_rrset(
dkim_name.as_str(),
DnsRecordType::TXT,
300,
vec![DnsRecord::TXT(dkim_value)],
origin.as_str(),
)
.await
.expect("set_rrset long TXT idempotent");
let listed = updater
.list_contents_for_tests(dkim_name.as_str(), DnsRecordType::TXT, origin.as_str())
.await
.expect("list long TXT");
assert_eq!(listed.len(), 1, "expected 1 long TXT, got {}", listed.len());
updater
.set_rrset(
dkim_name.as_str(),
DnsRecordType::TXT,
300,
vec![],
origin.as_str(),
)
.await
.expect("cleanup long TXT");
let list_name = label("list");
eprintln!("[phase 8.1] list_rrset on a never-set owner returns empty");
let empty = updater
.list_rrset(list_name.as_str(), DnsRecordType::A, origin.as_str())
.await
.expect("list_rrset on absent owner");
assert!(empty.is_empty(), "expected empty, got {empty:?}");
eprintln!("[phase 8.2] set_rrset then list_rrset round-trips A records");
let want_addrs = vec![
DnsRecord::A([10, 90, 0, 1].into()),
DnsRecord::A([10, 90, 0, 2].into()),
];
updater
.set_rrset(
list_name.as_str(),
DnsRecordType::A,
300,
want_addrs.clone(),
origin.as_str(),
)
.await
.expect("set A");
let mut got_addrs = updater
.list_rrset(list_name.as_str(), DnsRecordType::A, origin.as_str())
.await
.expect("list A");
got_addrs.sort_by_key(|r| match r {
DnsRecord::A(a) => *a,
_ => unreachable!(),
});
let mut want_sorted = want_addrs.clone();
want_sorted.sort_by_key(|r| match r {
DnsRecord::A(a) => *a,
_ => unreachable!(),
});
assert_eq!(got_addrs, want_sorted, "list_rrset A roundtrip mismatch");
eprintln!("[phase 8.3] add_to_rrset with already-present value is a no-op");
updater
.add_to_rrset(
list_name.as_str(),
DnsRecordType::A,
300,
vec![DnsRecord::A([10, 90, 0, 1].into())],
origin.as_str(),
)
.await
.expect("add existing");
let after_dup_add = updater
.list_rrset(list_name.as_str(), DnsRecordType::A, origin.as_str())
.await
.expect("list after dup add");
assert_eq!(
after_dup_add.len(),
2,
"add_to_rrset re-added an existing value: {after_dup_add:?}"
);
eprintln!("[phase 8.4] add_to_rrset with a new value increments");
updater
.add_to_rrset(
list_name.as_str(),
DnsRecordType::A,
300,
vec![DnsRecord::A([10, 90, 0, 3].into())],
origin.as_str(),
)
.await
.expect("add new");
let mut after_new_add = updater
.list_rrset(list_name.as_str(), DnsRecordType::A, origin.as_str())
.await
.expect("list after new add");
after_new_add.sort_by_key(|r| match r {
DnsRecord::A(a) => *a,
_ => unreachable!(),
});
assert_eq!(
after_new_add,
vec![
DnsRecord::A([10, 90, 0, 1].into()),
DnsRecord::A([10, 90, 0, 2].into()),
DnsRecord::A([10, 90, 0, 3].into()),
]
);
eprintln!("[phase 8.5] remove_from_rrset then list_rrset");
updater
.remove_from_rrset(
list_name.as_str(),
DnsRecordType::A,
vec![DnsRecord::A([10, 90, 0, 2].into())],
origin.as_str(),
)
.await
.expect("remove A2");
let mut after_remove = updater
.list_rrset(list_name.as_str(), DnsRecordType::A, origin.as_str())
.await
.expect("list after remove");
after_remove.sort_by_key(|r| match r {
DnsRecord::A(a) => *a,
_ => unreachable!(),
});
assert_eq!(
after_remove,
vec![
DnsRecord::A([10, 90, 0, 1].into()),
DnsRecord::A([10, 90, 0, 3].into()),
]
);
eprintln!("[phase 8.6] list_rrset round-trips a TLSA record by value");
let tlsa_list_name = srv("443", "tlsa-list");
let tlsa_record = DnsRecord::TLSA(TLSARecord {
cert_usage: TlsaCertUsage::DaneEe,
selector: TlsaSelector::Spki,
matching: TlsaMatching::Sha256,
cert_data: vec![0x77; 32],
});
updater
.set_rrset(
tlsa_list_name.as_str(),
DnsRecordType::TLSA,
300,
vec![tlsa_record.clone()],
origin.as_str(),
)
.await
.expect("set TLSA");
let tlsa_listed = updater
.list_rrset(
tlsa_list_name.as_str(),
DnsRecordType::TLSA,
origin.as_str(),
)
.await
.expect("list TLSA");
assert_eq!(
tlsa_listed,
vec![tlsa_record],
"list_rrset TLSA roundtrip mismatch"
);
eprintln!("[phase 8.7] cleanup");
updater
.set_rrset(
list_name.as_str(),
DnsRecordType::A,
300,
vec![],
origin.as_str(),
)
.await
.expect("cleanup list A");
updater
.set_rrset(
tlsa_list_name.as_str(),
DnsRecordType::TLSA,
300,
vec![],
origin.as_str(),
)
.await
.expect("cleanup TLSA");
eprintln!("[done] all live edge-case phases passed");
}
}