use crate::providers::route53::{Route53Config, Route53Provider};
use crate::{
CAARecord, DnsRecord, DnsRecordType, DnsUpdater, Error, MXRecord, SRVRecord, TLSARecord,
TlsaCertUsage, TlsaMatching, TlsaSelector,
};
use mockito::{Matcher, Mock, ServerGuard};
const ZONE_ID: &str = "Z1ABCDEFGH";
fn setup_provider(endpoint: String) -> Route53Provider {
Route53Provider::new(Route53Config {
access_key_id: "AKIA_TEST".to_string(),
secret_access_key: "test_secret".to_string(),
session_token: None,
region: Some("us-east-1".to_string()),
hosted_zone_id: Some(ZONE_ID.to_string()),
private_zone_only: Some(false),
})
.with_endpoint(endpoint)
}
fn setup_provider_zone_lookup(endpoint: String) -> Route53Provider {
Route53Provider::new(Route53Config {
access_key_id: "AKIA_TEST".to_string(),
secret_access_key: "test_secret".to_string(),
session_token: None,
region: Some("us-east-1".to_string()),
hosted_zone_id: None,
private_zone_only: Some(false),
})
.with_endpoint(endpoint)
}
fn list_path() -> String {
format!("/2013-04-01/hostedzone/{}/rrset", ZONE_ID)
}
fn change_path() -> String {
format!("/2013-04-01/hostedzone/{}/rrset", ZONE_ID)
}
fn list_response_xml(rrsets: &str) -> String {
format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<ListResourceRecordSetsResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
<ResourceRecordSets>{rrsets}</ResourceRecordSets>
<IsTruncated>false</IsTruncated>
<MaxItems>100</MaxItems>
</ListResourceRecordSetsResponse>"#
)
}
fn rrset_xml(name: &str, rtype: &str, ttl: u32, values: &[&str]) -> String {
let mut records = String::new();
for v in values {
records.push_str(&format!(
"<ResourceRecord><Value>{}</Value></ResourceRecord>",
v.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
));
}
format!(
"<ResourceRecordSet><Name>{name}</Name><Type>{rtype}</Type><TTL>{ttl}</TTL><ResourceRecords>{records}</ResourceRecords></ResourceRecordSet>"
)
}
fn change_response_xml() -> &'static str {
r#"<?xml version="1.0" encoding="UTF-8"?>
<ChangeResourceRecordSetsResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
<ChangeInfo>
<Id>/change/C123</Id>
<Status>PENDING</Status>
<SubmittedAt>2026-05-24T12:00:00Z</SubmittedAt>
</ChangeInfo>
</ChangeResourceRecordSetsResponse>"#
}
fn mock_list(server: &mut ServerGuard, name: &str, rtype: &str, body: String) -> Mock {
server
.mock("GET", list_path().as_str())
.match_query(Matcher::AllOf(vec![
Matcher::UrlEncoded("name".into(), name.into()),
Matcher::UrlEncoded("type".into(), rtype.into()),
]))
.with_status(200)
.with_header("content-type", "application/xml")
.with_body(body)
.create()
}
fn mock_change(server: &mut ServerGuard, body_substrings: Vec<String>) -> Mock {
let matchers: Vec<Matcher> = body_substrings.into_iter().map(Matcher::Regex).collect();
server
.mock("POST", change_path().as_str())
.match_body(Matcher::AllOf(matchers))
.with_status(200)
.with_header("content-type", "application/xml")
.with_body(change_response_xml())
.create()
}
fn mock_empty_rrset_list(server: &mut ServerGuard, name: &str, rtype: &str) -> Mock {
mock_list(server, name, rtype, list_response_xml(""))
}
#[tokio::test]
async fn test_route53_provider_creation() {
let _provider = Route53Provider::new(Route53Config {
access_key_id: "test_access_key".to_string(),
secret_access_key: "test_secret_key".to_string(),
session_token: None,
region: Some("us-east-1".to_string()),
hosted_zone_id: Some("test_zone_id".to_string()),
private_zone_only: Some(false),
});
}
#[tokio::test]
async fn test_route53_updater_creation() {
let updater = DnsUpdater::new_route53(Route53Config {
access_key_id: "test_access_key".to_string(),
secret_access_key: "test_secret_key".to_string(),
session_token: None,
region: Some("us-west-2".to_string()),
hosted_zone_id: None,
private_zone_only: Some(true),
})
.unwrap();
match updater {
DnsUpdater::Route53(_) => {}
_ => panic!("Expected Route53 provider"),
}
}
#[tokio::test]
async fn test_route53_config_defaults() {
let _provider = Route53Provider::new(Route53Config {
access_key_id: "test_access_key".to_string(),
secret_access_key: "test_secret_key".to_string(),
session_token: None,
region: None,
hosted_zone_id: None,
private_zone_only: None,
});
}
#[tokio::test]
async fn test_route53_config_with_session_token() {
let _provider = Route53Provider::new(Route53Config {
access_key_id: "test_access_key".to_string(),
secret_access_key: "test_secret_key".to_string(),
session_token: Some("test_session_token".to_string()),
region: Some("eu-west-1".to_string()),
hosted_zone_id: Some("Z1234567890".to_string()),
private_zone_only: Some(true),
});
}
#[tokio::test]
async fn test_route53_config_minimal() {
let updater = DnsUpdater::new_route53(Route53Config {
access_key_id: "test_access_key".to_string(),
secret_access_key: "test_secret_key".to_string(),
session_token: None,
region: None,
hosted_zone_id: None,
private_zone_only: None,
})
.unwrap();
match updater {
DnsUpdater::Route53(_) => {}
_ => panic!("Expected Route53 provider"),
}
}
#[tokio::test]
async fn test_set_rrset_upsert_with_records() {
let mut server = mockito::Server::new_async().await;
let change = mock_change(
&mut server,
vec![
r"<Action>UPSERT</Action>".to_string(),
r"<Name>host\.example\.com\.</Name>".to_string(),
r"<Type>A</Type>".to_string(),
r"<TTL>300</TTL>".to_string(),
r"<Value>1\.2\.3\.4</Value>".to_string(),
r"<Value>5\.6\.7\.8</Value>".to_string(),
],
);
let provider = setup_provider(server.url());
let result = provider
.set_rrset(
"host.example.com",
DnsRecordType::A,
300,
vec![
DnsRecord::A("1.2.3.4".parse().unwrap()),
DnsRecord::A("5.6.7.8".parse().unwrap()),
],
"example.com",
)
.await;
assert!(result.is_ok(), "set_rrset returned: {result:?}");
change.assert();
}
#[tokio::test]
async fn test_set_rrset_empty_deletes_existing() {
let mut server = mockito::Server::new_async().await;
let list = mock_list(
&mut server,
"gone.example.com.",
"A",
list_response_xml(&rrset_xml(
"gone.example.com.",
"A",
300,
&["1.2.3.4", "5.6.7.8"],
)),
);
let change = mock_change(
&mut server,
vec![
r"<Action>DELETE</Action>".to_string(),
r"<Name>gone\.example\.com\.</Name>".to_string(),
r"<Type>A</Type>".to_string(),
r"<Value>1\.2\.3\.4</Value>".to_string(),
r"<Value>5\.6\.7\.8</Value>".to_string(),
],
);
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:?}");
list.assert();
change.assert();
}
#[tokio::test]
async fn test_set_rrset_empty_noop_when_absent() {
let mut server = mockito::Server::new_async().await;
let list = mock_empty_rrset_list(&mut server, "missing.example.com.", "A");
let no_change = server
.mock("POST", change_path().as_str())
.expect(0)
.create();
let provider = setup_provider(server.url());
let result = provider
.set_rrset(
"missing.example.com",
DnsRecordType::A,
300,
vec![],
"example.com",
)
.await;
assert!(result.is_ok(), "set_rrset returned: {result:?}");
list.assert();
no_change.assert();
}
#[tokio::test]
async fn test_set_rrset_records_must_match_declared_type() {
let mut server = mockito::Server::new_async().await;
let no_call = server.mock("POST", Matcher::Any).expect(0).create();
let provider = setup_provider(server.url());
let result = provider
.set_rrset(
"host.example.com",
DnsRecordType::A,
300,
vec![DnsRecord::TXT("oops".to_string())],
"example.com",
)
.await;
assert!(matches!(result, Err(Error::Api(_))), "got {result:?}");
no_call.assert();
}
#[tokio::test]
async fn test_add_to_rrset_empty_is_early_return() {
let mut server = mockito::Server::new_async().await;
let _no_call = server.mock("GET", Matcher::Any).expect(0).create();
let _no_post = server.mock("POST", Matcher::Any).expect(0).create();
let provider = setup_provider(server.url());
let result = provider
.add_to_rrset(
"host.example.com",
DnsRecordType::A,
300,
vec![],
"example.com",
)
.await;
assert!(result.is_ok(), "add_to_rrset returned: {result:?}");
}
#[tokio::test]
async fn test_add_to_rrset_skips_already_present_value() {
let mut server = mockito::Server::new_async().await;
let list = mock_list(
&mut server,
"host.example.com.",
"A",
list_response_xml(&rrset_xml("host.example.com.", "A", 300, &["1.2.3.4"])),
);
let change = mock_change(
&mut server,
vec![
r"<Action>UPSERT</Action>".to_string(),
r"<Value>1\.2\.3\.4</Value>".to_string(),
r"<Value>9\.9\.9\.9</Value>".to_string(),
],
);
let provider = setup_provider(server.url());
let result = provider
.add_to_rrset(
"host.example.com",
DnsRecordType::A,
300,
vec![
DnsRecord::A("1.2.3.4".parse().unwrap()),
DnsRecord::A("9.9.9.9".parse().unwrap()),
],
"example.com",
)
.await;
assert!(result.is_ok(), "add_to_rrset returned: {result:?}");
list.assert();
change.assert();
}
#[tokio::test]
async fn test_add_to_rrset_creates_when_absent() {
let mut server = mockito::Server::new_async().await;
let list = mock_empty_rrset_list(&mut server, "fresh.example.com.", "A");
let change = mock_change(
&mut server,
vec![
r"<Action>UPSERT</Action>".to_string(),
r"<Name>fresh\.example\.com\.</Name>".to_string(),
r"<Value>9\.9\.9\.9</Value>".to_string(),
],
);
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:?}");
list.assert();
change.assert();
}
#[tokio::test]
async fn test_remove_from_rrset_empty_is_early_return() {
let mut server = mockito::Server::new_async().await;
let _no_call = server.mock("GET", Matcher::Any).expect(0).create();
let _no_post = server.mock("POST", Matcher::Any).expect(0).create();
let provider = setup_provider(server.url());
let result = provider
.remove_from_rrset("host.example.com", DnsRecordType::A, vec![], "example.com")
.await;
assert!(result.is_ok(), "remove_from_rrset returned: {result:?}");
}
#[tokio::test]
async fn test_remove_from_rrset_noop_when_value_absent() {
let mut server = mockito::Server::new_async().await;
let list = mock_list(
&mut server,
"host.example.com.",
"A",
list_response_xml(&rrset_xml("host.example.com.", "A", 300, &["1.2.3.4"])),
);
let _no_change = server
.mock("POST", change_path().as_str())
.expect(0)
.create();
let provider = setup_provider(server.url());
let result = provider
.remove_from_rrset(
"host.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:?}");
list.assert();
}
#[tokio::test]
async fn test_remove_from_rrset_drops_filtered_entry() {
let mut server = mockito::Server::new_async().await;
let list = mock_list(
&mut server,
"host.example.com.",
"A",
list_response_xml(&rrset_xml(
"host.example.com.",
"A",
600,
&["1.2.3.4", "9.9.9.9"],
)),
);
let change = mock_change(
&mut server,
vec![
r"<Action>UPSERT</Action>".to_string(),
r"<TTL>600</TTL>".to_string(),
r"<Value>1\.2\.3\.4</Value>".to_string(),
],
);
let provider = setup_provider(server.url());
let result = provider
.remove_from_rrset(
"host.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:?}");
list.assert();
change.assert();
}
#[tokio::test]
async fn test_remove_from_rrset_deletes_when_last_value_removed() {
let mut server = mockito::Server::new_async().await;
let list = mock_list(
&mut server,
"host.example.com.",
"A",
list_response_xml(&rrset_xml("host.example.com.", "A", 600, &["1.2.3.4"])),
);
let change = mock_change(
&mut server,
vec![
r"<Action>DELETE</Action>".to_string(),
r"<TTL>600</TTL>".to_string(),
r"<Value>1\.2\.3\.4</Value>".to_string(),
],
);
let provider = setup_provider(server.url());
let result = provider
.remove_from_rrset(
"host.example.com",
DnsRecordType::A,
vec![DnsRecord::A("1.2.3.4".parse().unwrap())],
"example.com",
)
.await;
assert!(result.is_ok(), "remove_from_rrset returned: {result:?}");
list.assert();
change.assert();
}
#[tokio::test]
async fn test_list_rrset_returns_parsed_records() {
let mut server = mockito::Server::new_async().await;
let list = mock_list(
&mut server,
"host.example.com.",
"A",
list_response_xml(&rrset_xml(
"host.example.com.",
"A",
300,
&["1.1.1.1", "2.2.2.2"],
)),
);
let provider = setup_provider(server.url());
let result = provider
.list_rrset("host.example.com", DnsRecordType::A, "example.com")
.await
.unwrap();
assert_eq!(
result,
vec![
DnsRecord::A("1.1.1.1".parse().unwrap()),
DnsRecord::A("2.2.2.2".parse().unwrap()),
]
);
list.assert();
}
#[tokio::test]
async fn test_list_rrset_empty_when_not_found() {
let mut server = mockito::Server::new_async().await;
let list = mock_empty_rrset_list(&mut server, "host.example.com.", "A");
let provider = setup_provider(server.url());
let result = provider
.list_rrset("host.example.com", DnsRecordType::A, "example.com")
.await
.unwrap();
assert!(result.is_empty());
list.assert();
}
#[tokio::test]
async fn test_list_rrset_cross_type_isolation() {
let mut server = mockito::Server::new_async().await;
let body = list_response_xml(&format!(
"{}{}",
rrset_xml("host.example.com.", "A", 300, &["1.1.1.1"]),
rrset_xml("host.example.com.", "TXT", 300, &["\"keep-me\""]),
));
let list = mock_list(&mut server, "host.example.com.", "A", body);
let provider = setup_provider(server.url());
let result = provider
.list_rrset("host.example.com", DnsRecordType::A, "example.com")
.await
.unwrap();
assert_eq!(result, vec![DnsRecord::A("1.1.1.1".parse().unwrap())]);
list.assert();
}
#[tokio::test]
async fn test_set_rrset_only_touches_requested_type() {
let mut server = mockito::Server::new_async().await;
let body = list_response_xml(&format!(
"{}{}",
rrset_xml("host.example.com.", "A", 300, &["1.1.1.1"]),
rrset_xml("host.example.com.", "TXT", 300, &["\"keep-me\""]),
));
let _list = mock_list(&mut server, "host.example.com.", "A", body);
let change = mock_change(
&mut server,
vec![
r"<Action>UPSERT</Action>".to_string(),
r"<Type>A</Type>".to_string(),
r"<Value>2\.2\.2\.2</Value>".to_string(),
],
);
let txt_change = server
.mock("POST", change_path().as_str())
.match_body(Matcher::Regex(r"<Type>TXT</Type>".to_string()))
.expect(0)
.create();
let provider = setup_provider(server.url());
let result = provider
.set_rrset(
"host.example.com",
DnsRecordType::A,
300,
vec![DnsRecord::A("2.2.2.2".parse().unwrap())],
"example.com",
)
.await;
assert!(result.is_ok(), "set_rrset returned: {result:?}");
change.assert();
txt_change.assert();
}
#[tokio::test]
async fn test_set_rrset_mx_with_two_priorities() {
let mut server = mockito::Server::new_async().await;
let change = mock_change(
&mut server,
vec![
r"<Action>UPSERT</Action>".to_string(),
r"<Type>MX</Type>".to_string(),
r"<Value>10 mx1\.example\.com\.</Value>".to_string(),
r"<Value>20 mx2\.example\.com\.</Value>".to_string(),
],
);
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:?}");
change.assert();
}
#[tokio::test]
async fn test_set_rrset_tlsa_two_records_same_owner() {
let mut server = mockito::Server::new_async().await;
let change = mock_change(
&mut server,
vec![
r"<Action>UPSERT</Action>".to_string(),
r"<Type>TLSA</Type>".to_string(),
r"<Value>3 1 1 aa</Value>".to_string(),
r"<Value>2 1 1 bb</Value>".to_string(),
],
);
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![0xaa],
}),
DnsRecord::TLSA(TLSARecord {
cert_usage: TlsaCertUsage::DaneTa,
selector: TlsaSelector::Spki,
matching: TlsaMatching::Sha256,
cert_data: vec![0xbb],
}),
],
"example.com",
)
.await;
assert!(result.is_ok(), "set_rrset returned: {result:?}");
change.assert();
}
#[tokio::test]
async fn test_set_rrset_caa() {
let mut server = mockito::Server::new_async().await;
let change = mock_change(
&mut server,
vec![
r"<Action>UPSERT</Action>".to_string(),
r"<Type>CAA</Type>".to_string(),
r#"0 issue "letsencrypt\.org""#.to_string(),
],
);
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:?}");
change.assert();
}
#[tokio::test]
async fn test_set_rrset_srv() {
let mut server = mockito::Server::new_async().await;
let change = mock_change(
&mut server,
vec![
r"<Action>UPSERT</Action>".to_string(),
r"<Type>SRV</Type>".to_string(),
r"<Value>10 5 993 mail\.example\.com\.</Value>".to_string(),
],
);
let provider = setup_provider(server.url());
let result = provider
.set_rrset(
"_imaps._tcp.example.com",
DnsRecordType::SRV,
300,
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:?}");
change.assert();
}
#[tokio::test]
async fn test_set_rrset_long_txt_is_chunked() {
let mut server = mockito::Server::new_async().await;
let change = mock_change(
&mut server,
vec![
r"<Action>UPSERT</Action>".to_string(),
r"<Type>TXT</Type>".to_string(),
],
);
let provider = setup_provider(server.url());
let long_value: String = "x".repeat(400);
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:?}");
change.assert();
}
#[tokio::test]
async fn test_zone_lookup_strips_hostedzone_prefix() {
let mut server = mockito::Server::new_async().await;
let zone_xml = format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<ListHostedZonesByNameResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
<HostedZones>
<HostedZone>
<Id>/hostedzone/{ZONE_ID}</Id>
<Name>example.com.</Name>
<CallerReference>ref</CallerReference>
<Config><PrivateZone>false</PrivateZone></Config>
</HostedZone>
</HostedZones>
<IsTruncated>false</IsTruncated>
<MaxItems>100</MaxItems>
</ListHostedZonesByNameResponse>"#
);
let zone_mock = server
.mock("GET", "/2013-04-01/hostedzonesbyname")
.with_status(200)
.with_header("content-type", "application/xml")
.with_body(zone_xml)
.create();
let change = mock_change(
&mut server,
vec![
r"<Action>UPSERT</Action>".to_string(),
r"<Value>1\.1\.1\.1</Value>".to_string(),
],
);
let provider = setup_provider_zone_lookup(server.url());
let result = provider
.set_rrset(
"host.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_mock.assert();
change.assert();
}
#[tokio::test]
async fn test_session_token_is_signed() {
let mut server = mockito::Server::new_async().await;
let change = server
.mock("POST", change_path().as_str())
.match_header("x-amz-security-token", "SESSION123")
.match_body(Matcher::Regex(r"<Action>UPSERT</Action>".to_string()))
.with_status(200)
.with_body(change_response_xml())
.expect(1)
.create();
let endpoint = server.url();
let provider = Route53Provider::new(Route53Config {
access_key_id: "AKIA_TEST".to_string(),
secret_access_key: "test_secret".to_string(),
session_token: Some("SESSION123".to_string()),
region: Some("us-east-1".to_string()),
hosted_zone_id: Some(ZONE_ID.to_string()),
private_zone_only: Some(false),
})
.with_endpoint(endpoint);
let result = provider
.set_rrset(
"host.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:?}");
change.assert();
}
#[tokio::test]
async fn test_unauthorized_response_maps_to_error_unauthorized() {
let mut server = mockito::Server::new_async().await;
let _ = server
.mock("POST", change_path().as_str())
.with_status(401)
.with_body("<Error><Code>InvalidSignature</Code></Error>")
.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())],
"example.com",
)
.await;
assert!(
matches!(result, Err(Error::Unauthorized)),
"expected Unauthorized, got {result:?}"
);
}
#[tokio::test]
async fn test_list_rrset_filters_to_requested_type_only() {
let mut server = mockito::Server::new_async().await;
let body = list_response_xml(&format!(
"{}{}",
rrset_xml("host.example.com.", "A", 300, &["1.1.1.1"]),
rrset_xml("host.example.com.", "AAAA", 300, &["::1"]),
));
let _list = mock_list(&mut server, "host.example.com.", "A", body);
let provider = setup_provider(server.url());
let result = provider
.list_rrset("host.example.com", DnsRecordType::A, "example.com")
.await
.unwrap();
assert_eq!(result, vec![DnsRecord::A("1.1.1.1".parse().unwrap())]);
}
#[tokio::test]
async fn test_set_rrset_txt_with_quotes() {
let mut server = mockito::Server::new_async().await;
let change = mock_change(
&mut server,
vec![
r"<Action>UPSERT</Action>".to_string(),
r"<Type>TXT</Type>".to_string(),
],
);
let provider = setup_provider(server.url());
let result = provider
.set_rrset(
"quoted.example.com",
DnsRecordType::TXT,
300,
vec![DnsRecord::TXT(r#"with "quote" inside"#.to_string())],
"example.com",
)
.await;
assert!(result.is_ok(), "set_rrset returned: {result:?}");
change.assert();
}
#[tokio::test]
async fn test_add_to_rrset_type_validation() {
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(
"host.example.com",
DnsRecordType::A,
300,
vec![DnsRecord::TXT("nope".to_string())],
"example.com",
)
.await;
assert!(matches!(result, Err(Error::Api(_))), "got {result:?}");
}
#[tokio::test]
async fn test_remove_from_rrset_type_validation() {
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
.remove_from_rrset(
"host.example.com",
DnsRecordType::A,
vec![DnsRecord::TXT("nope".to_string())],
"example.com",
)
.await;
assert!(matches!(result, Err(Error::Api(_))), "got {result:?}");
}
#[tokio::test]
#[ignore = "Requires AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, ROUTE53_ORIGIN and ROUTE53_HOSTED_ZONE_ID"]
async fn integration_test() {
let access_key = std::env::var("AWS_ACCESS_KEY_ID").unwrap_or_default();
let secret = std::env::var("AWS_SECRET_ACCESS_KEY").unwrap_or_default();
let origin = std::env::var("ROUTE53_ORIGIN").unwrap_or_default();
let hosted_zone_id = std::env::var("ROUTE53_HOSTED_ZONE_ID").ok();
assert!(!access_key.is_empty(), "Set AWS_ACCESS_KEY_ID");
assert!(!secret.is_empty(), "Set AWS_SECRET_ACCESS_KEY");
assert!(!origin.is_empty(), "Set ROUTE53_ORIGIN (e.g. example.com)");
let run_id = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let host = format!("dnsupdate-poc-{run_id}.{origin}");
let provider = Route53Provider::new(Route53Config {
access_key_id: access_key,
secret_access_key: secret,
session_token: None,
region: Some("us-east-1".to_string()),
hosted_zone_id,
private_zone_only: Some(false),
});
provider
.set_rrset(
host.as_str(),
DnsRecordType::A,
60,
vec![
DnsRecord::A("192.0.2.1".parse().unwrap()),
DnsRecord::A("192.0.2.2".parse().unwrap()),
],
origin.as_str(),
)
.await
.expect("set_rrset two A");
let listed = provider
.list_rrset(host.as_str(), DnsRecordType::A, origin.as_str())
.await
.expect("list_rrset");
assert_eq!(listed.len(), 2);
provider
.add_to_rrset(
host.as_str(),
DnsRecordType::A,
60,
vec![DnsRecord::A("192.0.2.3".parse().unwrap())],
origin.as_str(),
)
.await
.expect("add_to_rrset");
let listed = provider
.list_rrset(host.as_str(), DnsRecordType::A, origin.as_str())
.await
.expect("list_rrset after add");
assert_eq!(listed.len(), 3);
provider
.remove_from_rrset(
host.as_str(),
DnsRecordType::A,
vec![DnsRecord::A("192.0.2.2".parse().unwrap())],
origin.as_str(),
)
.await
.expect("remove_from_rrset");
let listed = provider
.list_rrset(host.as_str(), DnsRecordType::A, origin.as_str())
.await
.expect("list_rrset after remove");
assert_eq!(listed.len(), 2);
provider
.set_rrset(host.as_str(), DnsRecordType::A, 60, vec![], origin.as_str())
.await
.expect("set_rrset empty");
let listed = provider
.list_rrset(host.as_str(), DnsRecordType::A, origin.as_str())
.await
.expect("list_rrset after delete");
assert!(listed.is_empty());
}