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
25pub 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; 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
60pub 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); 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
96pub 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; 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}