#[cfg(test)]
mod tests {
use crate::{
DnsRecord, DnsRecordType, Error, MXRecord, providers::dnsmadeeasy::DnsMadeEasyProvider,
};
use mockito::Matcher;
use serde_json::json;
use std::time::Duration;
fn setup_provider(endpoint: String) -> DnsMadeEasyProvider {
DnsMadeEasyProvider::new("api_key", "api_secret", Some(Duration::from_secs(2)))
.unwrap()
.with_endpoint(endpoint)
}
fn mock_domain_lookup(server: &mut mockito::ServerGuard) -> mockito::Mock {
server
.mock("GET", "/dns/managed/name")
.match_query(Matcher::UrlEncoded(
"domainname".into(),
"example.com".into(),
))
.match_header("x-dnsme-apiKey", "api_key")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"id":12345,"name":"example.com"}"#)
.create()
}
#[tokio::test]
async fn test_set_rrset_empty_creates_via_bulk() {
let mut server = mockito::Server::new_async().await;
let domain_lookup = mock_domain_lookup(&mut server);
let record_lookup = server
.mock("GET", "/dns/managed/12345/records")
.match_query(Matcher::AllOf(vec![
Matcher::UrlEncoded("recordName".into(), "test".into()),
Matcher::UrlEncoded("type".into(), "A".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"data":[]}"#)
.create();
let create_multi = server
.mock("POST", "/dns/managed/12345/records/createMulti")
.match_header("x-dnsme-apiKey", "api_key")
.match_body(Matcher::Json(json!([
{"type": "A", "name": "test", "value": "1.2.3.4", "ttl": 300},
{"type": "A", "name": "test", "value": "5.6.7.8", "ttl": 300}
])))
.with_status(201)
.with_header("content-type", "application/json")
.with_body(r#"[{"id":101},{"id":102}]"#)
.create();
let provider = setup_provider(server.url());
let result = provider
.set_rrset(
"test.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:?}");
domain_lookup.assert();
record_lookup.assert();
create_multi.assert();
}
#[tokio::test]
async fn test_set_rrset_idempotent_no_changes() {
let mut server = mockito::Server::new_async().await;
let domain_lookup = mock_domain_lookup(&mut server);
let record_lookup = server
.mock("GET", "/dns/managed/12345/records")
.match_query(Matcher::AllOf(vec![
Matcher::UrlEncoded("recordName".into(), "test".into()),
Matcher::UrlEncoded("type".into(), "A".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"data":[{"id":1,"type":"A","value":"1.2.3.4","ttl":300},{"id":2,"type":"A","value":"5.6.7.8","ttl":300}]}"#)
.create();
let provider = setup_provider(server.url());
let result = provider
.set_rrset(
"test.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:?}");
domain_lookup.assert();
record_lookup.assert();
}
#[tokio::test]
async fn test_set_rrset_diff_add_and_remove() {
let mut server = mockito::Server::new_async().await;
let domain_lookup = mock_domain_lookup(&mut server);
let record_lookup = server
.mock("GET", "/dns/managed/12345/records")
.match_query(Matcher::AllOf(vec![
Matcher::UrlEncoded("recordName".into(), "test".into()),
Matcher::UrlEncoded("type".into(), "A".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"data":[{"id":10,"type":"A","value":"1.2.3.4","ttl":300},{"id":11,"type":"A","value":"9.9.9.9","ttl":300}]}"#)
.create();
let bulk_delete = server
.mock("DELETE", "/dns/managed/12345/records")
.match_query(Matcher::UrlEncoded("ids".into(), "11".into()))
.with_status(200)
.with_header("content-type", "application/json")
.with_body("{}")
.create();
let bulk_create = server
.mock("POST", "/dns/managed/12345/records/createMulti")
.match_body(Matcher::Json(json!([
{"type": "A", "name": "test", "value": "8.8.8.8", "ttl": 300}
])))
.with_status(201)
.with_header("content-type", "application/json")
.with_body(r#"[{"id":20}]"#)
.create();
let provider = setup_provider(server.url());
let result = provider
.set_rrset(
"test.example.com",
DnsRecordType::A,
300,
vec![
DnsRecord::A("1.2.3.4".parse().unwrap()),
DnsRecord::A("8.8.8.8".parse().unwrap()),
],
"example.com",
)
.await;
assert!(result.is_ok(), "set_rrset returned: {result:?}");
domain_lookup.assert();
record_lookup.assert();
bulk_delete.assert();
bulk_create.assert();
}
#[tokio::test]
async fn test_set_rrset_empty_vec_deletes_all() {
let mut server = mockito::Server::new_async().await;
let domain_lookup = mock_domain_lookup(&mut server);
let record_lookup = server
.mock("GET", "/dns/managed/12345/records")
.match_query(Matcher::AllOf(vec![
Matcher::UrlEncoded("recordName".into(), "test".into()),
Matcher::UrlEncoded("type".into(), "A".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"data":[{"id":1,"type":"A","value":"1.2.3.4","ttl":300},{"id":2,"type":"A","value":"5.6.7.8","ttl":300}]}"#)
.create();
let bulk_delete = server
.mock("DELETE", "/dns/managed/12345/records")
.match_query(Matcher::Regex("ids=1&ids=2".into()))
.with_status(200)
.with_header("content-type", "application/json")
.with_body("{}")
.create();
let provider = setup_provider(server.url());
let result = provider
.set_rrset(
"test.example.com",
DnsRecordType::A,
300,
vec![],
"example.com",
)
.await;
assert!(result.is_ok(), "set_rrset returned: {result:?}");
domain_lookup.assert();
record_lookup.assert();
bulk_delete.assert();
}
#[tokio::test]
async fn test_set_rrset_empty_vec_no_existing_no_calls() {
let mut server = mockito::Server::new_async().await;
let domain_lookup = mock_domain_lookup(&mut server);
let record_lookup = server
.mock("GET", "/dns/managed/12345/records")
.match_query(Matcher::AllOf(vec![
Matcher::UrlEncoded("recordName".into(), "test".into()),
Matcher::UrlEncoded("type".into(), "A".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"data":[]}"#)
.create();
let provider = setup_provider(server.url());
let result = provider
.set_rrset(
"test.example.com",
DnsRecordType::A,
300,
vec![],
"example.com",
)
.await;
assert!(result.is_ok(), "set_rrset returned: {result:?}");
domain_lookup.assert();
record_lookup.assert();
}
#[tokio::test]
async fn test_set_rrset_cross_type_isolation() {
let mut server = mockito::Server::new_async().await;
let domain_lookup = mock_domain_lookup(&mut server);
let record_lookup = server
.mock("GET", "/dns/managed/12345/records")
.match_query(Matcher::AllOf(vec![
Matcher::UrlEncoded("recordName".into(), "test".into()),
Matcher::UrlEncoded("type".into(), "A".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"data":[]}"#)
.create();
let create_multi = server
.mock("POST", "/dns/managed/12345/records/createMulti")
.match_body(Matcher::Json(json!([
{"type": "A", "name": "test", "value": "1.2.3.4", "ttl": 300}
])))
.with_status(201)
.with_header("content-type", "application/json")
.with_body(r#"[{"id":101}]"#)
.create();
let provider = setup_provider(server.url());
let result = provider
.set_rrset(
"test.example.com",
DnsRecordType::A,
300,
vec![DnsRecord::A("1.2.3.4".parse().unwrap())],
"example.com",
)
.await;
assert!(result.is_ok(), "set_rrset returned: {result:?}");
domain_lookup.assert();
record_lookup.assert();
create_multi.assert();
}
#[tokio::test]
async fn test_set_rrset_type_validation() {
let server = mockito::Server::new_async().await;
let provider = setup_provider(server.url());
let result = provider
.set_rrset(
"test.example.com",
DnsRecordType::A,
300,
vec![DnsRecord::TXT("oops".into())],
"example.com",
)
.await;
assert!(
matches!(result, Err(Error::Api(ref m)) if m.contains("type mismatch")),
"expected Api(type mismatch), got {result:?}"
);
}
#[tokio::test]
async fn test_set_rrset_tlsa_rejected() {
let server = mockito::Server::new_async().await;
let provider = setup_provider(server.url());
let result = provider
.set_rrset(
"test.example.com",
DnsRecordType::TLSA,
300,
vec![],
"example.com",
)
.await;
assert!(
matches!(result, Err(Error::Unsupported(ref m)) if m.contains("TLSA")),
"expected Unsupported(TLSA), got {result:?}"
);
}
#[tokio::test]
async fn test_add_to_rrset_empty_noop() {
let server = mockito::Server::new_async().await;
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_add_to_rrset_filters_missing() {
let mut server = mockito::Server::new_async().await;
let domain_lookup = mock_domain_lookup(&mut server);
let record_lookup = server
.mock("GET", "/dns/managed/12345/records")
.match_query(Matcher::AllOf(vec![
Matcher::UrlEncoded("recordName".into(), "test".into()),
Matcher::UrlEncoded("type".into(), "A".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"data":[{"id":5,"type":"A","value":"1.2.3.4","ttl":300}]}"#)
.create();
let create_multi = server
.mock("POST", "/dns/managed/12345/records/createMulti")
.match_body(Matcher::Json(json!([
{"type": "A", "name": "test", "value": "5.6.7.8", "ttl": 300}
])))
.with_status(201)
.with_header("content-type", "application/json")
.with_body(r#"[{"id":6}]"#)
.create();
let provider = setup_provider(server.url());
let result = provider
.add_to_rrset(
"test.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(), "add_to_rrset returned: {result:?}");
domain_lookup.assert();
record_lookup.assert();
create_multi.assert();
}
#[tokio::test]
async fn test_add_to_rrset_all_already_present_no_post() {
let mut server = mockito::Server::new_async().await;
let domain_lookup = mock_domain_lookup(&mut server);
let record_lookup = server
.mock("GET", "/dns/managed/12345/records")
.match_query(Matcher::AllOf(vec![
Matcher::UrlEncoded("recordName".into(), "test".into()),
Matcher::UrlEncoded("type".into(), "A".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"data":[{"id":5,"type":"A","value":"1.2.3.4","ttl":300}]}"#)
.create();
let provider = setup_provider(server.url());
let result = provider
.add_to_rrset(
"test.example.com",
DnsRecordType::A,
300,
vec![DnsRecord::A("1.2.3.4".parse().unwrap())],
"example.com",
)
.await;
assert!(result.is_ok(), "add_to_rrset returned: {result:?}");
domain_lookup.assert();
record_lookup.assert();
}
#[tokio::test]
async fn test_remove_from_rrset_empty_noop() {
let server = mockito::Server::new_async().await;
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_remove_from_rrset_filters_present_bulk_delete() {
let mut server = mockito::Server::new_async().await;
let domain_lookup = mock_domain_lookup(&mut server);
let record_lookup = server
.mock("GET", "/dns/managed/12345/records")
.match_query(Matcher::AllOf(vec![
Matcher::UrlEncoded("recordName".into(), "test".into()),
Matcher::UrlEncoded("type".into(), "A".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"data":[{"id":50,"type":"A","value":"1.2.3.4","ttl":300},{"id":51,"type":"A","value":"5.6.7.8","ttl":300}]}"#)
.create();
let bulk_delete = server
.mock("DELETE", "/dns/managed/12345/records")
.match_query(Matcher::Regex("ids=50&ids=51".into()))
.with_status(200)
.with_header("content-type", "application/json")
.with_body("{}")
.create();
let provider = setup_provider(server.url());
let result = provider
.remove_from_rrset(
"test.example.com",
DnsRecordType::A,
vec![
DnsRecord::A("1.2.3.4".parse().unwrap()),
DnsRecord::A("5.6.7.8".parse().unwrap()),
DnsRecord::A("9.9.9.9".parse().unwrap()),
],
"example.com",
)
.await;
assert!(result.is_ok(), "remove_from_rrset returned: {result:?}");
domain_lookup.assert();
record_lookup.assert();
bulk_delete.assert();
}
#[tokio::test]
async fn test_remove_from_rrset_none_present_no_delete() {
let mut server = mockito::Server::new_async().await;
let domain_lookup = mock_domain_lookup(&mut server);
let record_lookup = server
.mock("GET", "/dns/managed/12345/records")
.match_query(Matcher::AllOf(vec![
Matcher::UrlEncoded("recordName".into(), "test".into()),
Matcher::UrlEncoded("type".into(), "A".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"data":[{"id":50,"type":"A","value":"1.2.3.4","ttl":300}]}"#)
.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:?}");
domain_lookup.assert();
record_lookup.assert();
}
#[tokio::test]
async fn test_list_rrset_mx_parses_mxlevel() {
let mut server = mockito::Server::new_async().await;
let domain_lookup = mock_domain_lookup(&mut server);
let record_lookup = server
.mock("GET", "/dns/managed/12345/records")
.match_query(Matcher::AllOf(vec![
Matcher::UrlEncoded("recordName".into(), "@".into()),
Matcher::UrlEncoded("type".into(), "MX".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"data":[{"id":1,"type":"MX","value":"mail.example.com.","ttl":3600,"mxLevel":10},{"id":2,"type":"MX","value":"mail2.example.com.","ttl":3600,"mxLevel":20}]}"#)
.create();
let provider = setup_provider(server.url());
let result = provider
.list_rrset("example.com", DnsRecordType::MX, "example.com")
.await;
assert!(result.is_ok(), "list_rrset returned: {result:?}");
let records = result.unwrap();
assert_eq!(records.len(), 2);
assert!(records.contains(&DnsRecord::MX(MXRecord {
exchange: "mail.example.com.".into(),
priority: 10
})));
assert!(records.contains(&DnsRecord::MX(MXRecord {
exchange: "mail2.example.com.".into(),
priority: 20
})));
domain_lookup.assert();
record_lookup.assert();
}
#[tokio::test]
async fn test_set_rrset_mx_uses_mxlevel_field() {
let mut server = mockito::Server::new_async().await;
let domain_lookup = mock_domain_lookup(&mut server);
let record_lookup = server
.mock("GET", "/dns/managed/12345/records")
.match_query(Matcher::AllOf(vec![
Matcher::UrlEncoded("recordName".into(), "@".into()),
Matcher::UrlEncoded("type".into(), "MX".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"data":[]}"#)
.create();
let create_multi = server
.mock("POST", "/dns/managed/12345/records/createMulti")
.match_body(Matcher::Json(json!([
{"type": "MX", "name": "@", "value": "mail.example.com.", "ttl": 3600, "mxLevel": 10}
])))
.with_status(201)
.with_header("content-type", "application/json")
.with_body(r#"[{"id":42}]"#)
.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.".into(),
priority: 10,
})],
"example.com",
)
.await;
assert!(result.is_ok(), "set_rrset returned: {result:?}");
domain_lookup.assert();
record_lookup.assert();
create_multi.assert();
}
}