#[cfg(test)]
mod tests {
use crate::{
CAARecord, DnsRecord, DnsRecordType, DnsUpdater, Error, MXRecord, SRVRecord, TLSARecord,
TlsaCertUsage, TlsaMatching, TlsaSelector,
providers::ovh::{OvhEndpoint, OvhProvider, OvhRecordFormat},
};
use serde_json::json;
use std::time::Duration;
fn setup_provider() -> OvhProvider {
OvhProvider::new(
"test_app_key",
"test_app_secret",
"test_consumer_key",
OvhEndpoint::OvhEu,
Some(Duration::from_secs(1)),
)
.unwrap()
}
#[test]
fn test_ovh_endpoint_parsing() {
assert!(matches!(
"ovh-eu".parse::<OvhEndpoint>().unwrap(),
OvhEndpoint::OvhEu
));
assert!(matches!(
"ovh-ca".parse::<OvhEndpoint>().unwrap(),
OvhEndpoint::OvhCa
));
assert!(matches!(
"kimsufi-eu".parse::<OvhEndpoint>().unwrap(),
OvhEndpoint::KimsufiEu
));
assert!(matches!(
"kimsufi-ca".parse::<OvhEndpoint>().unwrap(),
OvhEndpoint::KimsufiCa
));
assert!(matches!(
"soyoustart-eu".parse::<OvhEndpoint>().unwrap(),
OvhEndpoint::SoyoustartEu
));
assert!(matches!(
"soyoustart-ca".parse::<OvhEndpoint>().unwrap(),
OvhEndpoint::SoyoustartCa
));
assert!("invalid-endpoint".parse::<OvhEndpoint>().is_err());
}
#[test]
fn test_ovh_provider_creation() {
let provider = OvhProvider::new(
"test_app_key",
"test_app_secret",
"test_consumer_key",
OvhEndpoint::OvhEu,
Some(Duration::from_secs(30)),
);
assert!(provider.is_ok());
}
#[test]
fn test_dns_updater_ovh_creation() {
let updater = DnsUpdater::new_ovh(
"test_app_key",
"test_app_secret",
"test_consumer_key",
OvhEndpoint::OvhEu,
Some(Duration::from_secs(30)),
);
assert!(updater.is_ok());
match updater.unwrap() {
DnsUpdater::Ovh(_) => (),
_ => panic!("Expected OVH provider"),
}
}
#[test]
fn test_ovh_record_format_from_dns_record() {
let record = DnsRecord::A("1.1.1.1".parse().unwrap());
let ovh_record: OvhRecordFormat = (&record).into();
assert_eq!(ovh_record.field_type, "A");
assert_eq!(ovh_record.target, "1.1.1.1");
let record = DnsRecord::AAAA("2001:db8::1".parse().unwrap());
let ovh_record: OvhRecordFormat = (&record).into();
assert_eq!(ovh_record.field_type, "AAAA");
assert_eq!(ovh_record.target, "2001:db8::1");
let record = DnsRecord::CNAME("alias.example.com".to_string());
let ovh_record: OvhRecordFormat = (&record).into();
assert_eq!(ovh_record.field_type, "CNAME");
assert_eq!(ovh_record.target, "alias.example.com");
let record = DnsRecord::MX(MXRecord {
exchange: "mail.example.com".to_string(),
priority: 10,
});
let ovh_record: OvhRecordFormat = (&record).into();
assert_eq!(ovh_record.field_type, "MX");
assert_eq!(ovh_record.target, "10 mail.example.com");
let record = DnsRecord::TXT("v=spf1 include:_spf.example.com ~all".to_string());
let ovh_record: OvhRecordFormat = (&record).into();
assert_eq!(ovh_record.field_type, "TXT");
assert_eq!(ovh_record.target, "v=spf1 include:_spf.example.com ~all");
let record = DnsRecord::SRV(SRVRecord {
target: "sip.example.com".to_string(),
priority: 10,
weight: 20,
port: 443,
});
let ovh_record: OvhRecordFormat = (&record).into();
assert_eq!(ovh_record.field_type, "SRV");
assert_eq!(ovh_record.target, "10 20 443 sip.example.com");
let record = DnsRecord::NS("ns1.example.com".to_string());
let ovh_record: OvhRecordFormat = (&record).into();
assert_eq!(ovh_record.field_type, "NS");
assert_eq!(ovh_record.target, "ns1.example.com");
}
#[tokio::test]
async fn test_create_record_success() {
let mut server = mockito::Server::new_async().await;
let zone_mock = server
.mock("GET", "/domain/zone/example.com")
.with_status(200)
.match_header("x-ovh-application", "test_app_key")
.match_header("x-ovh-consumer", "test_consumer_key")
.with_body(r#"{"name": "example.com"}"#)
.create();
let create_mock = server
.mock("POST", "/domain/zone/example.com/record")
.with_status(200)
.match_header("x-ovh-application", "test_app_key")
.match_header("x-ovh-consumer", "test_consumer_key")
.match_header("content-type", "application/json")
.match_body(mockito::Matcher::Json(json!({
"fieldType": "A",
"subDomain": "test",
"target": "1.1.1.1",
"ttl": 3600
})))
.with_body(r#"{"id": 123456789}"#)
.create();
let refresh_mock = server
.mock("POST", "/domain/zone/example.com/refresh")
.with_status(200)
.match_header("x-ovh-application", "test_app_key")
.match_header("x-ovh-consumer", "test_consumer_key")
.with_body("")
.create();
let mut provider = setup_provider();
provider.endpoint = server.url();
let result = provider
.create(
"test.example.com",
DnsRecord::A("1.1.1.1".parse().unwrap()),
3600,
"example.com",
)
.await;
assert!(result.is_ok());
zone_mock.assert();
create_mock.assert();
refresh_mock.assert();
}
#[tokio::test]
async fn test_update_record_success() {
let mut server = mockito::Server::new_async().await;
let zone_mock = server
.mock("GET", "/domain/zone/example.com")
.with_status(200)
.match_header("x-ovh-application", "test_app_key")
.match_header("x-ovh-consumer", "test_consumer_key")
.with_body(r#"{"name": "example.com"}"#)
.create();
let lookup_mock = server
.mock(
"GET",
"/domain/zone/example.com/record?fieldType=A&subDomain=test",
)
.with_status(200)
.match_header("x-ovh-application", "test_app_key")
.match_header("x-ovh-consumer", "test_consumer_key")
.with_body(r#"[123456789]"#)
.create();
let update_mock = server
.mock("PUT", "/domain/zone/example.com/record/123456789")
.with_status(200)
.match_header("x-ovh-application", "test_app_key")
.match_header("x-ovh-consumer", "test_consumer_key")
.match_header("content-type", "application/json")
.match_body(mockito::Matcher::Json(json!({
"target": "2.2.2.2",
"ttl": 3600
})))
.with_body("")
.create();
let refresh_mock = server
.mock("POST", "/domain/zone/example.com/refresh")
.with_status(200)
.match_header("x-ovh-application", "test_app_key")
.match_header("x-ovh-consumer", "test_consumer_key")
.with_body("")
.create();
let mut provider = setup_provider();
provider.endpoint = server.url();
let result = provider
.update(
"test.example.com",
DnsRecord::A("2.2.2.2".parse().unwrap()),
3600,
"example.com",
)
.await;
assert!(result.is_ok());
zone_mock.assert();
lookup_mock.assert();
update_mock.assert();
refresh_mock.assert();
}
#[tokio::test]
async fn test_delete_record_success() {
let mut server = mockito::Server::new_async().await;
let zone_mock = server
.mock("GET", "/domain/zone/example.com")
.with_status(200)
.match_header("x-ovh-application", "test_app_key")
.match_header("x-ovh-consumer", "test_consumer_key")
.with_body(r#"{"name": "example.com"}"#)
.create();
let lookup_mock = server
.mock(
"GET",
"/domain/zone/example.com/record?fieldType=TXT&subDomain=test",
)
.with_status(200)
.match_header("x-ovh-application", "test_app_key")
.match_header("x-ovh-consumer", "test_consumer_key")
.with_body(r#"[123456789]"#)
.create();
let delete_mock = server
.mock("DELETE", "/domain/zone/example.com/record/123456789")
.with_status(200)
.match_header("x-ovh-application", "test_app_key")
.match_header("x-ovh-consumer", "test_consumer_key")
.with_body("")
.create();
let refresh_mock = server
.mock("POST", "/domain/zone/example.com/refresh")
.with_status(200)
.match_header("x-ovh-application", "test_app_key")
.match_header("x-ovh-consumer", "test_consumer_key")
.with_body("")
.create();
let mut provider = setup_provider();
provider.endpoint = server.url();
let result = provider
.delete("test.example.com", "example.com", DnsRecordType::TXT)
.await;
assert!(result.is_ok());
zone_mock.assert();
lookup_mock.assert();
delete_mock.assert();
refresh_mock.assert();
}
#[tokio::test]
async fn test_create_record_unauthorized() {
let mut server = mockito::Server::new_async().await;
let zone_mock = server
.mock("GET", "/domain/zone/example.com")
.with_status(401)
.match_header("x-ovh-application", "test_app_key")
.match_header("x-ovh-consumer", "test_consumer_key")
.with_body(r#"{"message": "Invalid credentials"}"#)
.create();
let mut provider = setup_provider();
provider.endpoint = server.url();
let result = provider
.create(
"test.example.com",
DnsRecord::A("1.1.1.1".parse().unwrap()),
3600,
"example.com",
)
.await;
assert!(matches!(result, Err(Error::Api(_))));
zone_mock.assert();
}
#[tokio::test]
async fn test_record_not_found() {
let mut server = mockito::Server::new_async().await;
let zone_mock = server
.mock("GET", "/domain/zone/example.com")
.with_status(200)
.match_header("x-ovh-application", "test_app_key")
.match_header("x-ovh-consumer", "test_consumer_key")
.with_body(r#"{"name": "example.com"}"#)
.create();
let lookup_mock = server
.mock(
"GET",
"/domain/zone/example.com/record?fieldType=A&subDomain=nonexistent",
)
.with_status(200)
.match_header("x-ovh-application", "test_app_key")
.match_header("x-ovh-consumer", "test_consumer_key")
.with_body(r#"[]"#)
.create();
let mut provider = setup_provider();
provider.endpoint = server.url();
let result = provider
.update(
"nonexistent.example.com",
DnsRecord::A("1.1.1.1".parse().unwrap()),
3600,
"example.com",
)
.await;
assert!(matches!(result, Err(Error::NotFound)));
zone_mock.assert();
lookup_mock.assert();
}
#[tokio::test]
#[ignore = "Requires OVH API credentials and domain configuration"]
async fn integration_test() {
let app_key = std::env::var("OVH_APP_KEY").unwrap_or_default();
let app_secret = std::env::var("OVH_APP_SECRET").unwrap_or_default();
let consumer_key = std::env::var("OVH_CONSUMER_KEY").unwrap_or_default();
let endpoint = std::env::var("OVH_ENDPOINT").unwrap_or_default();
let origin = std::env::var("OVH_ORIGIN").unwrap_or_default();
let domain = std::env::var("OVH_DOMAIN").unwrap_or_default();
assert!(
!app_key.is_empty(),
"Please configure your OVH application key in the integration test"
);
assert!(
!app_secret.is_empty(),
"Please configure your OVH application secret in the integration test"
);
assert!(
!consumer_key.is_empty(),
"Please configure your OVH consumer key in the integration test"
);
assert!(
!endpoint.is_empty(),
"Please configure your endpoint in the integration test"
);
assert!(
!origin.is_empty(),
"Please configure your domain in the integration test"
);
assert!(
!domain.is_empty(),
"Please configure your test subdomain in the integration test"
);
let updater = DnsUpdater::new_ovh(
app_key,
app_secret,
consumer_key,
endpoint.parse().unwrap(),
Some(Duration::from_secs(30)),
)
.unwrap();
let creation_result = updater
.create(
&domain,
DnsRecord::A("1.1.1.1".parse().unwrap()),
3600,
&origin,
)
.await;
assert!(creation_result.is_ok());
let update_result = updater
.update(
&domain,
DnsRecord::A("2.2.2.2".parse().unwrap()),
3600,
&origin,
)
.await;
assert!(update_result.is_ok());
let deletion_result = updater.delete(&domain, &origin, DnsRecordType::A).await;
assert!(deletion_result.is_ok());
let tlsa_result = updater
.create(
&domain,
DnsRecord::TLSA(TLSARecord {
cert_usage: TlsaCertUsage::DaneEe,
selector: TlsaSelector::Spki,
matching: TlsaMatching::Sha256,
cert_data: vec![
0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8,
0x99, 0x6f, 0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c,
0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55,
],
}),
3600,
&origin,
)
.await;
assert!(tlsa_result.is_ok());
let tlsa_deletion_result = updater.delete(&domain, &origin, DnsRecordType::TLSA).await;
assert!(tlsa_deletion_result.is_ok());
let caa_result = updater
.create(
&domain,
DnsRecord::CAA(CAARecord::Issue {
issuer_critical: false,
name: Some("letsencrypt.org".to_string()),
options: vec![],
}),
3600,
&origin,
)
.await;
assert!(caa_result.is_ok());
let caa_deletion_result = updater.delete(&domain, &origin, DnsRecordType::CAA).await;
assert!(caa_deletion_result.is_ok());
}
}