netlify_ddns/
netlify.rs

1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use thiserror::Error;
4
5#[derive(Error, Debug)]
6pub enum NetlifyError {
7    #[error("The domain {domain} could not be found on your account.")]
8    MissingDomain { domain: String },
9    #[error("Unauthorized credentials. Check your Netlify token.")]
10    Unauthorized,
11    #[error("Netlify Error {status}: {op}.")]
12    Unknown { op: String, status: u16 },
13}
14
15#[derive(Serialize, Deserialize, Clone, Debug)]
16pub struct DnsRecord {
17    pub hostname: String,
18    #[serde(rename = "type")]
19    pub dns_type: String,
20    pub ttl: Option<u32>,
21    pub id: Option<String>,
22    pub value: String,
23}
24
25/// Retrieve the DNS records for domain, authenticated with token.
26pub fn get_dns_records(domain: &str, token: &str) -> Result<Vec<DnsRecord>> {
27    #[cfg(not(test))]
28    let url = format!(
29        "https://api.netlify.com/api/v1/dns_zones/{}/dns_records",
30        domain.replace('.', "_"),
31    );
32    #[cfg(test)]
33    let url = {
34        let _ = domain; // supress unused variable warning in test
35        mockito::server_url()
36    };
37
38    let resp = match ureq::get(&url).query("access_token", token).call() {
39        Ok(r) => r,
40        Err(ureq::Error::Status(code, _)) => {
41            return Err(NetlifyError::Unknown {
42                op: "Unable to get DNS records.".to_string(),
43                status: code,
44            }
45            .into())
46        }
47        Err(_) => {
48            return Err(NetlifyError::Unknown {
49                op: "Unable to get DNS records.".to_string(),
50                status: 0,
51            }
52            .into())
53        }
54    };
55
56    let dns_records: Vec<DnsRecord> = serde_json::from_str(&resp.into_string()?)?;
57    Ok(dns_records)
58}
59
60/// Delete the DNS record.
61pub fn delete_dns_record(domain: &str, token: &str, record: DnsRecord) -> Result<()> {
62    #[cfg(not(test))]
63    let url = format!(
64        "https://api.netlify.com/api/v1/dns_zones/{}/dns_records/{}",
65        domain.replace('.', "_"),
66        record.id.expect("Record did not have an ID."),
67    );
68    #[cfg(test)]
69    let url = {
70        let _ = (domain, record); // supress unused variable warning in test
71        mockito::server_url()
72    };
73
74    match ureq::delete(&url).query("access_token", token).call() {
75        Ok(_) => Ok(()),
76        Err(ureq::Error::Status(code, _)) => match code {
77            404 => Err(NetlifyError::MissingDomain {
78                domain: domain.to_string(),
79            }
80            .into()),
81            401 => Err(NetlifyError::Unauthorized.into()),
82            status => Err(NetlifyError::Unknown {
83                op: "could not delete dns record".to_string(),
84                status,
85            }
86            .into()),
87        },
88        Err(_) => Err(NetlifyError::Unknown {
89            op: "could not delete dns record".to_string(),
90            status: 0,
91        }
92        .into()),
93    }
94}
95
96/// Add a dns record to the domain.
97pub fn add_dns_record(domain: &str, token: &str, record: DnsRecord) -> Result<DnsRecord> {
98    #[cfg(not(test))]
99    let url = format!(
100        "https://api.netlify.com/api/v1/dns_zones/{}/dns_records",
101        domain.replace('.', "_"),
102    );
103    #[cfg(test)]
104    let url = {
105        let _ = domain; // supress unused variable warning in test
106        mockito::server_url()
107    };
108
109    let req = ureq::post(&url)
110        .query("access_token", token)
111        .set("Content-Type", "application/json");
112
113    match req.send_string(&serde_json::to_string(&record)?) {
114        Ok(r) => Ok(serde_json::from_str(&r.into_string()?)?),
115        Err(ureq::Error::Status(code, r)) => match code {
116            201 => Ok(serde_json::from_str(&r.into_string()?)?),
117            404 => Err(NetlifyError::MissingDomain {
118                domain: domain.to_string(),
119            }
120            .into()),
121            401 => Err(NetlifyError::Unauthorized.into()),
122            status => Err(NetlifyError::Unknown {
123                op: "could not add the dns record".to_string(),
124                status,
125            }
126            .into()),
127        },
128        Err(_) => Err(NetlifyError::Unknown {
129            op: "could not add the dns record".to_string(),
130            status: 0,
131        }
132        .into()),
133    }
134}
135
136#[cfg(test)]
137mod test {
138    use super::*;
139    use mockito::{mock, Matcher};
140
141    #[test]
142    fn test_get_dns_records() {
143        let body = "[{\"hostname\":\"www.example.com\",\"type\":\"NETLIFY\",\"ttl\":3600,\"priority\":null,\"weight\":null,\"port\":null,\"flag\":null,\"tag\":null,\"id\":\"\",\"site_id\":\"\",\"dns_zone_id\":\"\",\"errors\":[],\"managed\":true,\"value\":\"example.netlify.com\"},{\"hostname\":\"example.com\",\"type\":\"NETLIFY\",\"ttl\":3600,\"priority\":null,\"weight\":null,\"port\":null,\"flag\":null,\"tag\":null,\"id\":\"\",\"site_id\":\"\",\"dns_zone_id\":\"\",\"errors\":[],\"managed\":true,\"value\":\"example.netlify.com\"},{\"hostname\":\"www.example.com\",\"type\":\"NETLIFYv6\",\"ttl\":3600,\"priority\":null,\"weight\":null,\"port\":null,\"flag\":null,\"tag\":null,\"id\":\"\",\"site_id\":\"\",\"dns_zone_id\":\"\",\"errors\":[],\"managed\":true,\"value\":\"example.netlify.com\"},{\"hostname\":\"example.com\",\"type\":\"NETLIFYv6\",\"ttl\":3600,\"priority\":null,\"weight\":null,\"port\":null,\"flag\":null,\"tag\":null,\"id\":\"\",\"site_id\":\"\",\"dns_zone_id\":\"\",\"errors\":[],\"managed\":true,\"value\":\"example.netlify.com\"}]";
144
145        let _m = mock("GET", "/")
146            .match_query(Matcher::Regex("access_token.+$".into()))
147            .with_status(200)
148            .with_header("content-type", "application/json; charset=utf-8")
149            .with_header("content-length", &body.len().to_string())
150            .with_body(body)
151            .create();
152
153        let dns_records = get_dns_records("example.com", "token").unwrap();
154        assert_eq!(dns_records.len(), 4);
155    }
156
157    #[test]
158    fn test_delete_dns_records() {
159        let _m = mock("DELETE", "/")
160            .match_query(Matcher::Regex("access_token.+$".into()))
161            .with_status(200)
162            .create();
163
164        assert!(delete_dns_record(
165            "example.com",
166            "token",
167            DnsRecord {
168                hostname: String::from(""),
169                dns_type: String::from(""),
170                ttl: None,
171                id: Some(String::from("example")),
172                value: String::from("example"),
173            }
174        )
175        .is_ok());
176
177        let _m = mock("DELETE", "/")
178            .match_query(Matcher::Regex("access_token.+$".into()))
179            .with_status(404)
180            .create();
181
182        assert!(delete_dns_record(
183            "example.com",
184            "token",
185            DnsRecord {
186                hostname: String::from(""),
187                dns_type: String::from(""),
188                ttl: None,
189                id: Some(String::from("example")),
190                value: String::from("example"),
191            }
192        )
193        .is_err());
194    }
195
196    #[test]
197    fn test_add_dns_records() {
198        let body = "{\"hostname\":\"test.example.com\",\"type\":\"A\",\"ttl\":3600,\"priority\":null,\"weight\":null,\"port\":null,\"flag\":null,\"tag\":null,\"id\":\"\",\"site_id\":null,\"dns_zone_id\":\"\",\"errors\":[],\"managed\":false,\"value\":\"192.0.0.1\"}";
199
200        let _m = mock("POST", "/")
201            .match_query(Matcher::Regex("access_token.+$".into()))
202            .match_header("Content-Type", "application/json")
203            .with_status(201)
204            .with_header("content-type", "application/json; charset=utf-8")
205            .with_header("content-length", &body.len().to_string())
206            .with_body(body)
207            .create();
208
209        let resp = add_dns_record(
210            "example.com",
211            "token",
212            DnsRecord {
213                hostname: String::from(""),
214                dns_type: String::from(""),
215                ttl: None,
216                id: Some(String::from("")),
217                value: String::from(""),
218            },
219        )
220        .unwrap();
221        assert_eq!(resp.hostname, String::from("test.example.com"));
222        assert_eq!(resp.dns_type, String::from("A"));
223        assert_eq!(resp.ttl, Some(3600));
224        assert_eq!(resp.value, String::from("192.0.0.1"));
225        assert_eq!(resp.id, Some(String::from("")));
226    }
227}