dnslib/vendors/pangolin/
client.rs1use reqwest::Response;
2use serde_json::Value;
3
4use crate::core::error::{Error, Result};
5use crate::core::secret::ApiToken;
6use crate::vendors::http::HttpClient;
7
8#[derive(Clone, Debug)]
13pub struct PangolinClient {
14 http: HttpClient,
15 pub org_id: String,
16}
17
18impl PangolinClient {
19 pub fn new(base_url: String, token: ApiToken, org_id: String) -> Result<Self> {
20 Ok(Self {
21 http: HttpClient::new(base_url, token, false)?,
22 org_id,
23 })
24 }
25
26 pub fn base_url(&self) -> &str {
27 &self.http.base_url
28 }
29
30 pub async fn get(&self, path: &str, params: &[(&str, String)]) -> Result<Value> {
33 let req = self.http.get(path).query(params);
34 let resp = self.http.send("GET", path, req).await?;
35 parse_response(resp).await
36 }
37}
38
39async fn parse_response(resp: Response) -> Result<Value> {
40 let http_status = resp.status();
41
42 let body: Value = resp.json().await.map_err(|e| {
45 if e.is_decode() {
46 Error::InvalidJson(e)
47 } else {
48 Error::Network(e)
49 }
50 })?;
51
52 let success = body
53 .get("success")
54 .and_then(|s| s.as_bool())
55 .unwrap_or(false);
56 let api_status = body.get("status").and_then(|s| s.as_u64()).unwrap_or(0);
57
58 if success {
59 return Ok(body.get("data").cloned().unwrap_or(Value::Null));
60 }
61
62 let message = body
64 .get("message")
65 .and_then(|m| m.as_str())
66 .unwrap_or("unknown error")
67 .to_string();
68
69 if http_status.as_u16() == 403 || api_status == 403 {
71 return Err(Error::forbidden(message));
72 }
73
74 if body.get("error").and_then(|e| e.as_bool()).unwrap_or(false) || !http_status.is_success() {
76 return Err(Error::Api { message });
77 }
78
79 Err(Error::Http {
80 status: http_status.as_u16(),
81 body: body.to_string(),
82 })
83}
84
85#[cfg(test)]
88mod tests {
89 use super::*;
90 use serde_json::json;
91
92 fn make_resp(status: u16, body: Value) -> reqwest::Response {
93 http::Response::builder()
94 .status(status)
95 .header("content-type", "application/json")
96 .body(body.to_string())
97 .map(reqwest::Response::from)
98 .unwrap()
99 }
100
101 #[tokio::test]
102 async fn success_envelope_returns_data() {
103 let resp = make_resp(
104 200,
105 json!({
106 "data": { "orgs": [] },
107 "success": true,
108 "error": false,
109 "message": "ok",
110 "status": 200
111 }),
112 );
113 let val = parse_response(resp).await.unwrap();
114 assert_eq!(val, json!({ "orgs": [] }));
115 }
116
117 #[tokio::test]
118 async fn forbidden_envelope_returns_forbidden_error() {
119 let resp = make_resp(
120 403,
121 json!({
122 "data": null,
123 "success": false,
124 "error": true,
125 "message": "Key does not have root access",
126 "status": 403,
127 "stack": null
128 }),
129 );
130 let err = parse_response(resp).await.unwrap_err();
131 assert!(
132 matches!(err, Error::Forbidden { ref message } if message == "Key does not have root access")
133 );
134 }
135
136 #[tokio::test]
137 async fn api_error_envelope_returns_api_error() {
138 let resp = make_resp(
139 400,
140 json!({
141 "data": null,
142 "success": false,
143 "error": true,
144 "message": "zone not found",
145 "status": 400,
146 "stack": null
147 }),
148 );
149 let err = parse_response(resp).await.unwrap_err();
150 assert!(matches!(err, Error::Api { ref message } if message == "zone not found"));
151 }
152}