#[cfg(test)]
mod tests {
use crate::{
CAARecord, DnsRecord, DnsRecordType, DnsUpdater, Error, KeyValue, MXRecord, SRVRecord,
providers::glesys::GlesysProvider,
};
use serde_json::json;
use std::time::Duration;
fn setup_provider(endpoint: &str) -> GlesysProvider {
GlesysProvider::new("api_user", "api_key", Some(Duration::from_secs(5)))
.with_endpoint(endpoint)
}
#[tokio::test]
async fn test_set_rrset_empty_deletes_only_matching_type() {
let mut server = mockito::Server::new_async().await;
let list_mock = server
.mock("POST", "/domain/listrecords")
.match_body(mockito::Matcher::Json(json!({"domainname": "example.com"})))
.with_status(200)
.with_body(
r#"{"response": {"records": [
{"recordid": 1, "type": "A", "host": "www", "data": "1.1.1.1"},
{"recordid": 2, "type": "A", "host": "www", "data": "2.2.2.2"},
{"recordid": 3, "type": "TXT", "host": "www", "data": "keep"},
{"recordid": 4, "type": "A", "host": "other", "data": "9.9.9.9"}
]}}"#,
)
.create();
let del1 = server
.mock("POST", "/domain/deleterecord")
.match_body(mockito::Matcher::Json(json!({"recordid": 1})))
.with_status(200)
.with_body(r#"{"response": {"status": {"code": 200}}}"#)
.create();
let del2 = server
.mock("POST", "/domain/deleterecord")
.match_body(mockito::Matcher::Json(json!({"recordid": 2})))
.with_status(200)
.with_body(r#"{"response": {"status": {"code": 200}}}"#)
.create();
let provider = setup_provider(server.url().as_str());
let result = provider
.set_rrset(
"www.example.com",
DnsRecordType::A,
300,
Vec::new(),
"example.com",
)
.await;
assert!(result.is_ok(), "set_rrset empty failed: {result:?}");
list_mock.assert();
del1.assert();
del2.assert();
}
#[tokio::test]
async fn test_set_rrset_diff_adds_and_deletes() {
let mut server = mockito::Server::new_async().await;
let list_mock = server
.mock("POST", "/domain/listrecords")
.with_status(200)
.with_body(
r#"{"response": {"records": [
{"recordid": 1, "type": "A", "host": "www", "data": "1.1.1.1"},
{"recordid": 2, "type": "A", "host": "www", "data": "2.2.2.2"},
{"recordid": 3, "type": "TXT", "host": "www", "data": "untouched"}
]}}"#,
)
.create();
let del_old = server
.mock("POST", "/domain/deleterecord")
.match_body(mockito::Matcher::Json(json!({"recordid": 2})))
.with_status(200)
.with_body(r#"{"response": {"status": {"code": 200}}}"#)
.create();
let add_new = server
.mock("POST", "/domain/addrecord")
.match_body(mockito::Matcher::Json(json!({
"domainname": "example.com",
"host": "www",
"type": "A",
"data": "3.3.3.3",
"ttl": 300,
})))
.with_status(200)
.with_body(r#"{"response": {"record": {"recordid": 10}}}"#)
.create();
let provider = setup_provider(server.url().as_str());
let result = provider
.set_rrset(
"www.example.com",
DnsRecordType::A,
300,
vec![
DnsRecord::A("1.1.1.1".parse().unwrap()),
DnsRecord::A("3.3.3.3".parse().unwrap()),
],
"example.com",
)
.await;
assert!(result.is_ok(), "set_rrset diff failed: {result:?}");
list_mock.assert();
del_old.assert();
add_new.assert();
}
#[tokio::test]
async fn test_set_rrset_idempotent_no_calls() {
let mut server = mockito::Server::new_async().await;
let list_mock = server
.mock("POST", "/domain/listrecords")
.with_status(200)
.with_body(
r#"{"response": {"records": [
{"recordid": 1, "type": "A", "host": "www", "data": "1.1.1.1"}
]}}"#,
)
.create();
let add = server
.mock("POST", "/domain/addrecord")
.with_status(500)
.expect(0)
.create();
let del = server
.mock("POST", "/domain/deleterecord")
.with_status(500)
.expect(0)
.create();
let provider = setup_provider(server.url().as_str());
let result = provider
.set_rrset(
"www.example.com",
DnsRecordType::A,
300,
vec![DnsRecord::A("1.1.1.1".parse().unwrap())],
"example.com",
)
.await;
assert!(result.is_ok(), "set_rrset idempotent failed: {result:?}");
list_mock.assert();
add.assert();
del.assert();
}
#[tokio::test]
async fn test_set_rrset_mx_uses_data_priority() {
let mut server = mockito::Server::new_async().await;
let list_mock = server
.mock("POST", "/domain/listrecords")
.with_status(200)
.with_body(r#"{"response": {"records": []}}"#)
.create();
let add = server
.mock("POST", "/domain/addrecord")
.match_body(mockito::Matcher::Json(json!({
"domainname": "example.com",
"host": "@",
"type": "MX",
"data": "20 backup.example.com.",
"ttl": 3600,
})))
.with_status(200)
.with_body(r#"{"response": {"record": {"recordid": 11}}}"#)
.create();
let provider = setup_provider(server.url().as_str());
let result = provider
.set_rrset(
"example.com",
DnsRecordType::MX,
3600,
vec![DnsRecord::MX(MXRecord {
priority: 20,
exchange: "backup.example.com.".to_string(),
})],
"example.com",
)
.await;
assert!(result.is_ok(), "set_rrset MX failed: {result:?}");
list_mock.assert();
add.assert();
}
#[tokio::test]
async fn test_add_to_rrset_empty_short_circuits() {
let server = mockito::Server::new_async().await;
let provider = setup_provider(server.url().as_str());
let result = provider
.add_to_rrset(
"www.example.com",
DnsRecordType::A,
300,
Vec::new(),
"example.com",
)
.await;
assert!(result.is_ok(), "add empty failed: {result:?}");
}
#[tokio::test]
async fn test_add_to_rrset_skips_existing() {
let mut server = mockito::Server::new_async().await;
let list_mock = server
.mock("POST", "/domain/listrecords")
.with_status(200)
.with_body(
r#"{"response": {"records": [
{"recordid": 1, "type": "A", "host": "www", "data": "1.1.1.1"}
]}}"#,
)
.create();
let add = server
.mock("POST", "/domain/addrecord")
.match_body(mockito::Matcher::Json(json!({
"domainname": "example.com",
"host": "www",
"type": "A",
"data": "2.2.2.2",
"ttl": 300,
})))
.with_status(200)
.with_body(r#"{"response": {"record": {"recordid": 12}}}"#)
.create();
let provider = setup_provider(server.url().as_str());
let result = provider
.add_to_rrset(
"www.example.com",
DnsRecordType::A,
300,
vec![
DnsRecord::A("1.1.1.1".parse().unwrap()),
DnsRecord::A("2.2.2.2".parse().unwrap()),
],
"example.com",
)
.await;
assert!(result.is_ok(), "add_to_rrset failed: {result:?}");
list_mock.assert();
add.assert();
}
#[tokio::test]
async fn test_remove_from_rrset_empty_short_circuits() {
let server = mockito::Server::new_async().await;
let provider = setup_provider(server.url().as_str());
let result = provider
.remove_from_rrset(
"www.example.com",
DnsRecordType::A,
Vec::new(),
"example.com",
)
.await;
assert!(result.is_ok(), "remove empty failed: {result:?}");
}
#[tokio::test]
async fn test_remove_from_rrset_ignores_absent() {
let mut server = mockito::Server::new_async().await;
let list_mock = server
.mock("POST", "/domain/listrecords")
.with_status(200)
.with_body(
r#"{"response": {"records": [
{"recordid": 1, "type": "A", "host": "www", "data": "1.1.1.1"}
]}}"#,
)
.create();
let del = server
.mock("POST", "/domain/deleterecord")
.match_body(mockito::Matcher::Json(json!({"recordid": 1})))
.with_status(200)
.with_body(r#"{"response": {"status": {"code": 200}}}"#)
.create();
let provider = setup_provider(server.url().as_str());
let result = provider
.remove_from_rrset(
"www.example.com",
DnsRecordType::A,
vec![
DnsRecord::A("1.1.1.1".parse().unwrap()),
DnsRecord::A("9.9.9.9".parse().unwrap()),
],
"example.com",
)
.await;
assert!(result.is_ok(), "remove_from_rrset failed: {result:?}");
list_mock.assert();
del.assert();
}
#[tokio::test]
async fn test_list_rrset_filters_by_name_and_type() {
let mut server = mockito::Server::new_async().await;
let list_mock = server
.mock("POST", "/domain/listrecords")
.with_status(200)
.with_body(
r#"{"response": {"records": [
{"recordid": 1, "type": "A", "host": "www", "data": "1.1.1.1"},
{"recordid": 2, "type": "AAAA", "host": "www", "data": "2001:db8::1"},
{"recordid": 3, "type": "A", "host": "other", "data": "9.9.9.9"},
{"recordid": 4, "type": "A", "host": "www", "data": "2.2.2.2"}
]}}"#,
)
.create();
let provider = setup_provider(server.url().as_str());
let result = provider
.list_rrset("www.example.com", DnsRecordType::A, "example.com")
.await
.expect("list_rrset failed");
list_mock.assert();
assert_eq!(result.len(), 2);
assert!(result.contains(&DnsRecord::A("1.1.1.1".parse().unwrap())));
assert!(result.contains(&DnsRecord::A("2.2.2.2".parse().unwrap())));
}
#[tokio::test]
async fn test_list_rrset_parses_mx_srv_caa() {
let mut server = mockito::Server::new_async().await;
let list_mock = server
.mock("POST", "/domain/listrecords")
.with_status(200)
.with_body(
r#"{"response": {"records": [
{"recordid": 1, "type": "MX", "host": "@", "data": "10 mail.example.com."}
]}}"#,
)
.create();
let provider = setup_provider(server.url().as_str());
let result = provider
.list_rrset("example.com", DnsRecordType::MX, "example.com")
.await
.expect("list_rrset MX failed");
list_mock.assert();
assert_eq!(
result,
vec![DnsRecord::MX(MXRecord {
priority: 10,
exchange: "mail.example.com.".to_string(),
})]
);
let mut server2 = mockito::Server::new_async().await;
let list_mock2 = server2
.mock("POST", "/domain/listrecords")
.with_status(200)
.with_body(
r#"{"response": {"records": [
{"recordid": 1, "type": "SRV", "host": "_sip._tcp", "data": "10 20 5060 sip.example.com."}
]}}"#,
)
.create();
let provider2 = setup_provider(server2.url().as_str());
let result2 = provider2
.list_rrset("_sip._tcp.example.com", DnsRecordType::SRV, "example.com")
.await
.expect("list_rrset SRV failed");
list_mock2.assert();
assert_eq!(
result2,
vec![DnsRecord::SRV(SRVRecord {
priority: 10,
weight: 20,
port: 5060,
target: "sip.example.com.".to_string(),
})]
);
let mut server3 = mockito::Server::new_async().await;
let list_mock3 = server3
.mock("POST", "/domain/listrecords")
.with_status(200)
.with_body(
r#"{"response": {"records": [
{"recordid": 1, "type": "CAA", "host": "@", "data": "0 issue \"letsencrypt.org\""}
]}}"#,
)
.create();
let provider3 = setup_provider(server3.url().as_str());
let result3 = provider3
.list_rrset("example.com", DnsRecordType::CAA, "example.com")
.await
.expect("list_rrset CAA failed");
list_mock3.assert();
assert_eq!(
result3,
vec![DnsRecord::CAA(CAARecord::Issue {
issuer_critical: false,
name: Some("letsencrypt.org".to_string()),
options: Vec::<KeyValue>::new(),
})]
);
}
#[tokio::test]
async fn test_set_rrset_caa_encodes_bind_style() {
let mut server = mockito::Server::new_async().await;
let list_mock = server
.mock("POST", "/domain/listrecords")
.with_status(200)
.with_body(r#"{"response": {"records": []}}"#)
.create();
let add = server
.mock("POST", "/domain/addrecord")
.match_body(mockito::Matcher::Json(json!({
"domainname": "example.com",
"host": "@",
"type": "CAA",
"data": "0 issue \"letsencrypt.org\"",
"ttl": 3600,
})))
.with_status(200)
.with_body(r#"{"response": {"record": {"recordid": 1}}}"#)
.create();
let provider = setup_provider(server.url().as_str());
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 CAA failed: {result:?}");
list_mock.assert();
add.assert();
}
#[tokio::test]
async fn test_type_validation_rejects_wrong_type() {
let server = mockito::Server::new_async().await;
let provider = setup_provider(server.url().as_str());
let result = provider
.set_rrset(
"www.example.com",
DnsRecordType::A,
300,
vec![DnsRecord::TXT("oops".to_string())],
"example.com",
)
.await;
assert!(
matches!(result, Err(Error::Api(ref m)) if m.contains("type mismatch")),
"got {result:?}"
);
let result_add = provider
.add_to_rrset(
"www.example.com",
DnsRecordType::A,
300,
vec![DnsRecord::TXT("oops".to_string())],
"example.com",
)
.await;
assert!(
matches!(result_add, Err(Error::Api(ref m)) if m.contains("type mismatch")),
"got {result_add:?}"
);
let result_remove = provider
.remove_from_rrset(
"www.example.com",
DnsRecordType::A,
vec![DnsRecord::TXT("oops".to_string())],
"example.com",
)
.await;
assert!(
matches!(result_remove, Err(Error::Api(ref m)) if m.contains("type mismatch")),
"got {result_remove:?}"
);
}
#[tokio::test]
async fn test_tlsa_refused() {
use crate::{TLSARecord, TlsaCertUsage, TlsaMatching, TlsaSelector};
let server = mockito::Server::new_async().await;
let provider = setup_provider(server.url().as_str());
let result = provider
.set_rrset(
"_443._tcp.example.com",
DnsRecordType::TLSA,
3600,
vec![DnsRecord::TLSA(TLSARecord {
cert_usage: TlsaCertUsage::DaneEe,
selector: TlsaSelector::Spki,
matching: TlsaMatching::Sha256,
cert_data: vec![0u8; 32],
})],
"example.com",
)
.await;
assert!(
matches!(result, Err(Error::Unsupported(ref m)) if m.contains("TLSA")),
"got {result:?}"
);
}
#[tokio::test]
#[ignore = "Requires GleSYS credentials, zone, and FQDN"]
async fn integration_test() {
let user = std::env::var("GLESYS_API_USER").unwrap_or_default();
let key = std::env::var("GLESYS_API_KEY").unwrap_or_default();
let origin = std::env::var("GLESYS_ORIGIN").unwrap_or_default();
let fqdn = std::env::var("GLESYS_FQDN").unwrap_or_default();
assert!(!user.is_empty() && !key.is_empty());
assert!(!origin.is_empty() && !fqdn.is_empty());
let updater = DnsUpdater::new_glesys(user, key, Some(Duration::from_secs(30))).unwrap();
let set = updater
.set_rrset(
&fqdn,
DnsRecordType::A,
300,
vec![DnsRecord::A([1, 1, 1, 1].into())],
&origin,
)
.await;
assert!(set.is_ok(), "set_rrset failed: {set:?}");
let clear = updater
.set_rrset(&fqdn, DnsRecordType::A, 300, Vec::new(), &origin)
.await;
assert!(clear.is_ok(), "clear failed: {clear:?}");
}
#[test]
fn dns_updater_creation() {
let updater = DnsUpdater::new_glesys("u", "k", Some(Duration::from_secs(30)));
assert!(matches!(updater, Ok(DnsUpdater::Glesys(..))));
}
}