1use crate::error::{Error, Result};
8use log::debug;
9use reqwest::Client;
10use std::{net::Ipv4Addr, str::FromStr};
11
12pub const DIGITAL_OCEAN_API_BASE_URL: &str = "https://api.digitalocean.com";
13pub const DIGITAL_OCEAN_API_PAGE_SIZE: usize = 200;
14
15pub struct Droplet {
16 pub id: usize,
17 pub name: String,
18 pub ip_address: Ipv4Addr,
19}
20
21pub struct DigitalOceanClient {
22 pub base_url: String,
23 pub access_token: String,
24 pub page_size: usize,
25}
26
27impl DigitalOceanClient {
28 pub async fn list_droplets(&self, skip_if_no_ip: bool) -> Result<Vec<Droplet>> {
29 let client = Client::new();
30 let mut has_next_page = true;
31 let mut page = 1;
32 let mut droplets = Vec::new();
33 while has_next_page {
34 let url = format!(
35 "{}/v2/droplets?page={}&per_page={}",
36 self.base_url, page, self.page_size
37 );
38 debug!("Executing droplet list request with {url}");
39 let response = client
40 .get(url)
41 .header("Authorization", format!("Bearer {}", self.access_token))
42 .send()
43 .await?;
44 if response.status().as_u16() == 401 {
45 debug!("Error response body: {}", response.text().await?);
46 return Err(Error::DigitalOceanUnauthorized);
47 } else if !response.status().is_success() {
48 let status_code = response.status().as_u16();
49 let response_body = response.text().await?;
50 debug!("Response status code: {}", status_code);
51 debug!("Error response body: {}", response_body);
52 return Err(Error::DigitalOceanUnexpectedResponse(
53 status_code,
54 response_body,
55 ));
56 }
57
58 let json: serde_json::Value = serde_json::from_str(&response.text().await?)?;
59 let droplet_array =
60 json["droplets"]
61 .as_array()
62 .ok_or(Error::MalformedDigitalOceanApiRespose(
63 "droplets".to_string(),
64 ))?;
65
66 for droplet_json in droplet_array {
67 let id = droplet_json["id"]
68 .as_u64()
69 .ok_or(Error::MalformedDigitalOceanApiRespose("id".to_string()))?;
70 let name = droplet_json["name"]
71 .as_str()
72 .ok_or(Error::MalformedDigitalOceanApiRespose("name".to_string()))?
73 .to_string();
74 let ip_address_array = droplet_json["networks"]["v4"].as_array().ok_or(
75 Error::MalformedDigitalOceanApiRespose("droplets".to_string()),
76 )?;
77 let get_ip_address = || -> Result<Ipv4Addr, Error> {
79 let public_ip = ip_address_array
80 .iter()
81 .find(|x| x["type"].as_str().unwrap() == "public")
82 .ok_or(Error::DigitalOceanPublicIpAddressNotFound)?;
83
84 let ip_address = Ipv4Addr::from_str(
85 public_ip["ip_address"]
86 .as_str()
87 .ok_or(Error::DigitalOceanPublicIpAddressNotFound)?,
88 )?;
89
90 Ok(ip_address)
91 };
92
93 match get_ip_address() {
94 Ok(ip_address) => {
95 droplets.push(Droplet {
96 id: id as usize,
97 name,
98 ip_address,
99 });
100 }
101 Err(_) if skip_if_no_ip => continue,
102 Err(err) => return Err(err),
103 }
104 }
105
106 let links_object = json["links"]
107 .as_object()
108 .ok_or(Error::MalformedDigitalOceanApiRespose("links".to_string()))?;
109 if links_object.is_empty() {
110 has_next_page = false;
112 } else {
113 let pages_object = links_object["pages"]
114 .as_object()
115 .ok_or(Error::MalformedDigitalOceanApiRespose("pages".to_string()))?;
116 if pages_object.contains_key("next") {
117 page += 1;
118 } else {
119 has_next_page = false;
120 }
121 }
122 }
123
124 Ok(droplets)
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use color_eyre::{eyre::eyre, Result};
132 use httpmock::prelude::*;
133
134 #[tokio::test]
135 async fn test_list_droplets_with_single_page() -> Result<()> {
136 const MOCK_API_RESPONSE: &str = r#"
139 {
140 "droplets": [
141 {
142 "id": 118019015,
143 "name": "testnet-node-01",
144 "memory": 2048,
145 "vcpus": 1,
146 "disk": 50,
147 "locked": false,
148 "status": "active",
149 "kernel": null,
150 "created_at": "2018-11-05T13:57:25Z",
151 "features": [],
152 "backup_ids": [],
153 "next_backup_window": null,
154 "snapshot_ids": [
155 136060266
156 ],
157 "image": {
158 "id": 33346667,
159 "name": "Droplet Deployer OLD",
160 "distribution": "Ubuntu",
161 "slug": null,
162 "public": false,
163 "regions": [
164 "lon1",
165 "nyc1",
166 "sfo1",
167 "ams2",
168 "sgp1",
169 "fra1",
170 "tor1",
171 "blr1"
172 ],
173 "created_at": "2018-04-10T10:40:49Z",
174 "min_disk_size": 50,
175 "type": "snapshot",
176 "size_gigabytes": 7.11,
177 "tags": [],
178 "status": "available"
179 },
180 "volume_ids": [],
181 "size": {
182 "slug": "s-1vcpu-2gb",
183 "memory": 2048,
184 "vcpus": 1,
185 "disk": 50,
186 "transfer": 2,
187 "price_monthly": 12,
188 "price_hourly": 0.01786,
189 "regions": [
190 "ams3",
191 "blr1",
192 "fra1",
193 "lon1",
194 "nyc1",
195 "nyc3",
196 "sfo2",
197 "sfo3",
198 "sgp1",
199 "syd1",
200 "tor1"
201 ],
202 "available": true,
203 "description": "Basic"
204 },
205 "size_slug": "s-1vcpu-2gb",
206 "networks": {
207 "v4": [
208 {
209 "ip_address": "192.168.0.2",
210 "netmask": "255.255.240.0",
211 "gateway": "192.168.0.1",
212 "type": "private"
213 },
214 {
215 "ip_address": "104.248.0.110",
216 "netmask": "255.255.240.0",
217 "gateway": "192.168.0.1",
218 "type": "public"
219 }
220 ],
221 "v6": []
222 },
223 "region": {
224 "name": "London 1",
225 "slug": "lon1",
226 "features": [
227 "backups",
228 "ipv6",
229 "metadata",
230 "install_agent",
231 "storage",
232 "image_transfer"
233 ],
234 "available": true,
235 "sizes": [
236 "s-1vcpu-1gb",
237 "s-1vcpu-1gb-amd",
238 "s-1vcpu-1gb-intel",
239 "s-1vcpu-1gb-35gb-intel",
240 "s-1vcpu-2gb",
241 "s-1vcpu-2gb-amd",
242 "s-1vcpu-2gb-intel",
243 "s-1vcpu-2gb-70gb-intel",
244 "s-2vcpu-2gb",
245 "s-2vcpu-2gb-amd",
246 "s-2vcpu-2gb-intel",
247 "s-2vcpu-2gb-90gb-intel",
248 "s-2vcpu-4gb",
249 "s-2vcpu-4gb-amd",
250 "s-2vcpu-4gb-intel",
251 "s-2vcpu-4gb-120gb-intel",
252 "c-2",
253 "c2-2vcpu-4gb",
254 "s-4vcpu-8gb",
255 "s-4vcpu-8gb-amd",
256 "s-4vcpu-8gb-intel",
257 "g-2vcpu-8gb",
258 "s-4vcpu-8gb-240gb-intel",
259 "gd-2vcpu-8gb",
260 "m-2vcpu-16gb",
261 "c-4",
262 "c2-4vcpu-8gb",
263 "s-8vcpu-16gb",
264 "m3-2vcpu-16gb",
265 "s-8vcpu-16gb-amd",
266 "s-8vcpu-16gb-intel",
267 "g-4vcpu-16gb",
268 "s-8vcpu-16gb-480gb-intel",
269 "so-2vcpu-16gb",
270 "m6-2vcpu-16gb",
271 "gd-4vcpu-16gb",
272 "so1_5-2vcpu-16gb",
273 "m-4vcpu-32gb",
274 "c-8",
275 "c2-8vcpu-16gb",
276 "m3-4vcpu-32gb",
277 "g-8vcpu-32gb",
278 "so-4vcpu-32gb",
279 "m6-4vcpu-32gb",
280 "gd-8vcpu-32gb",
281 "so1_5-4vcpu-32gb",
282 "m-8vcpu-64gb",
283 "c-16",
284 "c2-16vcpu-32gb",
285 "m3-8vcpu-64gb",
286 "g-16vcpu-64gb",
287 "so-8vcpu-64gb",
288 "m6-8vcpu-64gb",
289 "gd-16vcpu-64gb",
290 "so1_5-8vcpu-64gb",
291 "m-16vcpu-128gb",
292 "c-32",
293 "c2-32vcpu-64gb",
294 "m3-16vcpu-128gb",
295 "c-48",
296 "m-24vcpu-192gb",
297 "g-32vcpu-128gb",
298 "so-16vcpu-128gb",
299 "m6-16vcpu-128gb",
300 "gd-32vcpu-128gb",
301 "c2-48vcpu-96gb",
302 "m3-24vcpu-192gb",
303 "g-40vcpu-160gb",
304 "so1_5-16vcpu-128gb",
305 "m-32vcpu-256gb",
306 "gd-40vcpu-160gb",
307 "so-24vcpu-192gb",
308 "m6-24vcpu-192gb",
309 "m3-32vcpu-256gb",
310 "so1_5-24vcpu-192gb",
311 "so-32vcpu-256gb",
312 "m6-32vcpu-256gb",
313 "so1_5-32vcpu-256gb"
314 ]
315 },
316 "tags": []
317 },
318 {
319 "id": 177884621,
320 "name": "testnet-node-02",
321 "memory": 2048,
322 "vcpus": 2,
323 "disk": 60,
324 "locked": false,
325 "status": "active",
326 "kernel": null,
327 "created_at": "2020-01-30T03:39:42Z",
328 "features": [],
329 "backup_ids": [],
330 "next_backup_window": null,
331 "snapshot_ids": [
332 136060570
333 ],
334 "image": {
335 "id": 53893572,
336 "name": "18.04.3 (LTS) x64",
337 "distribution": "Ubuntu",
338 "slug": null,
339 "public": false,
340 "regions": [],
341 "created_at": "2019-10-22T01:38:19Z",
342 "min_disk_size": 20,
343 "type": "base",
344 "size_gigabytes": 2.36,
345 "description": "Ubuntu 18.04 x64 20191022",
346 "tags": [],
347 "status": "deleted"
348 },
349 "volume_ids": [],
350 "size": {
351 "slug": "s-2vcpu-2gb",
352 "memory": 2048,
353 "vcpus": 2,
354 "disk": 60,
355 "transfer": 3,
356 "price_monthly": 18,
357 "price_hourly": 0.02679,
358 "regions": [
359 "ams3",
360 "blr1",
361 "fra1",
362 "lon1",
363 "nyc1",
364 "nyc3",
365 "sfo2",
366 "sfo3",
367 "sgp1",
368 "syd1",
369 "tor1"
370 ],
371 "available": true,
372 "description": "Basic"
373 },
374 "size_slug": "s-2vcpu-2gb",
375 "networks": {
376 "v4": [
377 {
378 "ip_address": "192.168.0.3",
379 "netmask": "255.255.240.0",
380 "gateway": "192.168.0.1",
381 "type": "private"
382 },
383 {
384 "ip_address": "104.248.0.111",
385 "netmask": "255.255.240.0",
386 "gateway": "192.168.0.1",
387 "type": "public"
388 }
389 ],
390 "v6": []
391 },
392 "region": {
393 "name": "New York 3",
394 "slug": "nyc3",
395 "features": [
396 "backups",
397 "ipv6",
398 "metadata",
399 "install_agent",
400 "storage",
401 "image_transfer"
402 ],
403 "available": true,
404 "sizes": [
405 "s-1vcpu-1gb",
406 "s-1vcpu-1gb-amd",
407 "s-1vcpu-1gb-intel",
408 "s-1vcpu-1gb-35gb-intel",
409 "s-1vcpu-2gb",
410 "s-1vcpu-2gb-amd",
411 "s-1vcpu-2gb-intel",
412 "s-1vcpu-2gb-70gb-intel",
413 "s-2vcpu-2gb",
414 "s-2vcpu-2gb-amd",
415 "s-2vcpu-2gb-intel",
416 "s-2vcpu-2gb-90gb-intel",
417 "s-2vcpu-4gb",
418 "s-2vcpu-4gb-amd",
419 "s-2vcpu-4gb-intel",
420 "s-2vcpu-4gb-120gb-intel",
421 "s-2vcpu-8gb-amd",
422 "c-2",
423 "c2-2vcpu-4gb",
424 "s-2vcpu-8gb-160gb-intel",
425 "s-4vcpu-8gb",
426 "s-4vcpu-8gb-amd",
427 "s-4vcpu-8gb-intel",
428 "g-2vcpu-8gb",
429 "s-4vcpu-8gb-240gb-intel",
430 "gd-2vcpu-8gb",
431 "s-4vcpu-16gb-amd",
432 "m-2vcpu-16gb",
433 "c-4",
434 "c2-4vcpu-8gb",
435 "s-4vcpu-16gb-320gb-intel",
436 "s-8vcpu-16gb",
437 "m3-2vcpu-16gb",
438 "c-4-intel",
439 "s-8vcpu-16gb-amd",
440 "s-8vcpu-16gb-intel",
441 "c2-4vcpu-8gb-intel",
442 "g-4vcpu-16gb",
443 "s-8vcpu-16gb-480gb-intel",
444 "so-2vcpu-16gb",
445 "m6-2vcpu-16gb",
446 "gd-4vcpu-16gb",
447 "so1_5-2vcpu-16gb",
448 "s-8vcpu-32gb-amd",
449 "m-4vcpu-32gb",
450 "c-8",
451 "c2-8vcpu-16gb",
452 "s-8vcpu-32gb-640gb-intel",
453 "m3-4vcpu-32gb",
454 "c-8-intel",
455 "c2-8vcpu-16gb-intel",
456 "g-8vcpu-32gb",
457 "so-4vcpu-32gb",
458 "m6-4vcpu-32gb",
459 "gd-8vcpu-32gb",
460 "so1_5-4vcpu-32gb",
461 "s-16vcpu-64gb-amd",
462 "m-8vcpu-64gb",
463 "c-16",
464 "c2-16vcpu-32gb",
465 "s-16vcpu-64gb-intel",
466 "m3-8vcpu-64gb",
467 "c-16-intel",
468 "c2-16vcpu-32gb-intel",
469 "g-16vcpu-64gb",
470 "so-8vcpu-64gb",
471 "m6-8vcpu-64gb",
472 "gd-16vcpu-64gb",
473 "so1_5-8vcpu-64gb",
474 "m-16vcpu-128gb",
475 "c-32",
476 "c2-32vcpu-64gb",
477 "m3-16vcpu-128gb",
478 "c-32-intel",
479 "c2-32vcpu-64gb-intel",
480 "c-48",
481 "m-24vcpu-192gb",
482 "g-32vcpu-128gb",
483 "so-16vcpu-128gb",
484 "m6-16vcpu-128gb",
485 "gd-32vcpu-128gb",
486 "c2-48vcpu-96gb",
487 "m3-24vcpu-192gb",
488 "g-40vcpu-160gb",
489 "so1_5-16vcpu-128gb",
490 "m-32vcpu-256gb",
491 "gd-40vcpu-160gb",
492 "so-24vcpu-192gb",
493 "m6-24vcpu-192gb",
494 "m3-32vcpu-256gb",
495 "so1_5-24vcpu-192gb",
496 "so-32vcpu-256gb",
497 "m6-32vcpu-256gb",
498 "so1_5-32vcpu-256gb"
499 ]
500 },
501 "tags": []
502 }
503 ],
504 "links": {},
505 "meta": {
506 "total": 104
507 }
508 }
509 "#;
510
511 let server = MockServer::start();
512 let list_droplets_mock = server.mock(|when, then| {
513 when.method(GET).path("/v2/droplets");
514 then.status(200)
515 .header("Content-Type", "application/json")
516 .body(MOCK_API_RESPONSE);
517 });
518
519 let client = DigitalOceanClient {
520 base_url: server.base_url(),
521 access_token: String::from("fake_token"),
522 page_size: DIGITAL_OCEAN_API_PAGE_SIZE,
523 };
524
525 let droplets = client.list_droplets(false).await?;
526
527 assert_eq!(2, droplets.len());
528 assert_eq!(118019015, droplets[0].id);
529 assert_eq!("testnet-node-01", droplets[0].name);
530 assert_eq!(
531 Ipv4Addr::from_str("104.248.0.110").unwrap(),
532 droplets[0].ip_address
533 );
534 assert_eq!(177884621, droplets[1].id);
535 assert_eq!("testnet-node-02", droplets[1].name);
536 assert_eq!(
537 Ipv4Addr::from_str("104.248.0.111").unwrap(),
538 droplets[1].ip_address
539 );
540
541 list_droplets_mock.assert();
542
543 Ok(())
544 }
545
546 #[tokio::test]
547 async fn test_list_droplets_with_paged_response() -> Result<()> {
548 const MOCK_API_PAGE_1_RESPONSE: &str = r#"
549 {
550 "droplets": [
551 {
552 "id": 118019015,
553 "name": "testnet-node-01",
554 "memory": 2048,
555 "vcpus": 1,
556 "disk": 50,
557 "locked": false,
558 "status": "active",
559 "kernel": null,
560 "created_at": "2018-11-05T13:57:25Z",
561 "features": [],
562 "backup_ids": [],
563 "next_backup_window": null,
564 "snapshot_ids": [
565 136060266
566 ],
567 "image": {
568 "id": 33346667,
569 "name": "Droplet Deployer OLD",
570 "distribution": "Ubuntu",
571 "slug": null,
572 "public": false,
573 "regions": [
574 "lon1",
575 "nyc1",
576 "sfo1",
577 "ams2",
578 "sgp1",
579 "fra1",
580 "tor1",
581 "blr1"
582 ],
583 "created_at": "2018-04-10T10:40:49Z",
584 "min_disk_size": 50,
585 "type": "snapshot",
586 "size_gigabytes": 7.11,
587 "tags": [],
588 "status": "available"
589 },
590 "volume_ids": [],
591 "size": {
592 "slug": "s-1vcpu-2gb",
593 "memory": 2048,
594 "vcpus": 1,
595 "disk": 50,
596 "transfer": 2,
597 "price_monthly": 12,
598 "price_hourly": 0.01786,
599 "regions": [
600 "ams3",
601 "blr1",
602 "fra1",
603 "lon1",
604 "nyc1",
605 "nyc3",
606 "sfo2",
607 "sfo3",
608 "sgp1",
609 "syd1",
610 "tor1"
611 ],
612 "available": true,
613 "description": "Basic"
614 },
615 "size_slug": "s-1vcpu-2gb",
616 "networks": {
617 "v4": [
618 {
619 "ip_address": "192.168.0.2",
620 "netmask": "255.255.240.0",
621 "gateway": "192.168.0.1",
622 "type": "private"
623 },
624 {
625 "ip_address": "104.248.0.110",
626 "netmask": "255.255.240.0",
627 "gateway": "192.168.0.1",
628 "type": "public"
629 }
630 ],
631 "v6": []
632 },
633 "region": {
634 "name": "London 1",
635 "slug": "lon1",
636 "features": [
637 "backups",
638 "ipv6",
639 "metadata",
640 "install_agent",
641 "storage",
642 "image_transfer"
643 ],
644 "available": true,
645 "sizes": [
646 "s-1vcpu-1gb",
647 "s-1vcpu-1gb-amd",
648 "s-1vcpu-1gb-intel",
649 "s-1vcpu-1gb-35gb-intel",
650 "s-1vcpu-2gb",
651 "s-1vcpu-2gb-amd",
652 "s-1vcpu-2gb-intel",
653 "s-1vcpu-2gb-70gb-intel",
654 "s-2vcpu-2gb",
655 "s-2vcpu-2gb-amd",
656 "s-2vcpu-2gb-intel",
657 "s-2vcpu-2gb-90gb-intel",
658 "s-2vcpu-4gb",
659 "s-2vcpu-4gb-amd",
660 "s-2vcpu-4gb-intel",
661 "s-2vcpu-4gb-120gb-intel",
662 "c-2",
663 "c2-2vcpu-4gb",
664 "s-4vcpu-8gb",
665 "s-4vcpu-8gb-amd",
666 "s-4vcpu-8gb-intel",
667 "g-2vcpu-8gb",
668 "s-4vcpu-8gb-240gb-intel",
669 "gd-2vcpu-8gb",
670 "m-2vcpu-16gb",
671 "c-4",
672 "c2-4vcpu-8gb",
673 "s-8vcpu-16gb",
674 "m3-2vcpu-16gb",
675 "s-8vcpu-16gb-amd",
676 "s-8vcpu-16gb-intel",
677 "g-4vcpu-16gb",
678 "s-8vcpu-16gb-480gb-intel",
679 "so-2vcpu-16gb",
680 "m6-2vcpu-16gb",
681 "gd-4vcpu-16gb",
682 "so1_5-2vcpu-16gb",
683 "m-4vcpu-32gb",
684 "c-8",
685 "c2-8vcpu-16gb",
686 "m3-4vcpu-32gb",
687 "g-8vcpu-32gb",
688 "so-4vcpu-32gb",
689 "m6-4vcpu-32gb",
690 "gd-8vcpu-32gb",
691 "so1_5-4vcpu-32gb",
692 "m-8vcpu-64gb",
693 "c-16",
694 "c2-16vcpu-32gb",
695 "m3-8vcpu-64gb",
696 "g-16vcpu-64gb",
697 "so-8vcpu-64gb",
698 "m6-8vcpu-64gb",
699 "gd-16vcpu-64gb",
700 "so1_5-8vcpu-64gb",
701 "m-16vcpu-128gb",
702 "c-32",
703 "c2-32vcpu-64gb",
704 "m3-16vcpu-128gb",
705 "c-48",
706 "m-24vcpu-192gb",
707 "g-32vcpu-128gb",
708 "so-16vcpu-128gb",
709 "m6-16vcpu-128gb",
710 "gd-32vcpu-128gb",
711 "c2-48vcpu-96gb",
712 "m3-24vcpu-192gb",
713 "g-40vcpu-160gb",
714 "so1_5-16vcpu-128gb",
715 "m-32vcpu-256gb",
716 "gd-40vcpu-160gb",
717 "so-24vcpu-192gb",
718 "m6-24vcpu-192gb",
719 "m3-32vcpu-256gb",
720 "so1_5-24vcpu-192gb",
721 "so-32vcpu-256gb",
722 "m6-32vcpu-256gb",
723 "so1_5-32vcpu-256gb"
724 ]
725 },
726 "tags": []
727 }
728 ],
729 "links": {
730 "pages": {
731 "next": "https://api.digitalocean.com/v2/droplets?page=1&per_page=1",
732 "last": "https://api.digitalocean.com/v2/droplets?page=2&per_page=1"
733 }
734 },
735 "meta": {
736 "total": 104
737 }
738 }
739 "#;
740 const MOCK_API_PAGE_2_RESPONSE: &str = r#"
741 {
742 "droplets": [
743 {
744 "id": 177884621,
745 "name": "testnet-node-02",
746 "memory": 2048,
747 "vcpus": 2,
748 "disk": 60,
749 "locked": false,
750 "status": "active",
751 "kernel": null,
752 "created_at": "2020-01-30T03:39:42Z",
753 "features": [],
754 "backup_ids": [],
755 "next_backup_window": null,
756 "snapshot_ids": [
757 136060570
758 ],
759 "image": {
760 "id": 53893572,
761 "name": "18.04.3 (LTS) x64",
762 "distribution": "Ubuntu",
763 "slug": null,
764 "public": false,
765 "regions": [],
766 "created_at": "2019-10-22T01:38:19Z",
767 "min_disk_size": 20,
768 "type": "base",
769 "size_gigabytes": 2.36,
770 "description": "Ubuntu 18.04 x64 20191022",
771 "tags": [],
772 "status": "deleted"
773 },
774 "volume_ids": [],
775 "size": {
776 "slug": "s-2vcpu-2gb",
777 "memory": 2048,
778 "vcpus": 2,
779 "disk": 60,
780 "transfer": 3,
781 "price_monthly": 18,
782 "price_hourly": 0.02679,
783 "regions": [
784 "ams3",
785 "blr1",
786 "fra1",
787 "lon1",
788 "nyc1",
789 "nyc3",
790 "sfo2",
791 "sfo3",
792 "sgp1",
793 "syd1",
794 "tor1"
795 ],
796 "available": true,
797 "description": "Basic"
798 },
799 "size_slug": "s-2vcpu-2gb",
800 "networks": {
801 "v4": [
802 {
803 "ip_address": "192.168.0.3",
804 "netmask": "255.255.240.0",
805 "gateway": "192.168.0.1",
806 "type": "private"
807 },
808 {
809 "ip_address": "104.248.0.111",
810 "netmask": "255.255.240.0",
811 "gateway": "192.168.0.1",
812 "type": "public"
813 }
814 ],
815 "v6": []
816 },
817 "region": {
818 "name": "New York 3",
819 "slug": "nyc3",
820 "features": [
821 "backups",
822 "ipv6",
823 "metadata",
824 "install_agent",
825 "storage",
826 "image_transfer"
827 ],
828 "available": true,
829 "sizes": [
830 "s-1vcpu-1gb",
831 "s-1vcpu-1gb-amd",
832 "s-1vcpu-1gb-intel",
833 "s-1vcpu-1gb-35gb-intel",
834 "s-1vcpu-2gb",
835 "s-1vcpu-2gb-amd",
836 "s-1vcpu-2gb-intel",
837 "s-1vcpu-2gb-70gb-intel",
838 "s-2vcpu-2gb",
839 "s-2vcpu-2gb-amd",
840 "s-2vcpu-2gb-intel",
841 "s-2vcpu-2gb-90gb-intel",
842 "s-2vcpu-4gb",
843 "s-2vcpu-4gb-amd",
844 "s-2vcpu-4gb-intel",
845 "s-2vcpu-4gb-120gb-intel",
846 "s-2vcpu-8gb-amd",
847 "c-2",
848 "c2-2vcpu-4gb",
849 "s-2vcpu-8gb-160gb-intel",
850 "s-4vcpu-8gb",
851 "s-4vcpu-8gb-amd",
852 "s-4vcpu-8gb-intel",
853 "g-2vcpu-8gb",
854 "s-4vcpu-8gb-240gb-intel",
855 "gd-2vcpu-8gb",
856 "s-4vcpu-16gb-amd",
857 "m-2vcpu-16gb",
858 "c-4",
859 "c2-4vcpu-8gb",
860 "s-4vcpu-16gb-320gb-intel",
861 "s-8vcpu-16gb",
862 "m3-2vcpu-16gb",
863 "c-4-intel",
864 "s-8vcpu-16gb-amd",
865 "s-8vcpu-16gb-intel",
866 "c2-4vcpu-8gb-intel",
867 "g-4vcpu-16gb",
868 "s-8vcpu-16gb-480gb-intel",
869 "so-2vcpu-16gb",
870 "m6-2vcpu-16gb",
871 "gd-4vcpu-16gb",
872 "so1_5-2vcpu-16gb",
873 "s-8vcpu-32gb-amd",
874 "m-4vcpu-32gb",
875 "c-8",
876 "c2-8vcpu-16gb",
877 "s-8vcpu-32gb-640gb-intel",
878 "m3-4vcpu-32gb",
879 "c-8-intel",
880 "c2-8vcpu-16gb-intel",
881 "g-8vcpu-32gb",
882 "so-4vcpu-32gb",
883 "m6-4vcpu-32gb",
884 "gd-8vcpu-32gb",
885 "so1_5-4vcpu-32gb",
886 "s-16vcpu-64gb-amd",
887 "m-8vcpu-64gb",
888 "c-16",
889 "c2-16vcpu-32gb",
890 "s-16vcpu-64gb-intel",
891 "m3-8vcpu-64gb",
892 "c-16-intel",
893 "c2-16vcpu-32gb-intel",
894 "g-16vcpu-64gb",
895 "so-8vcpu-64gb",
896 "m6-8vcpu-64gb",
897 "gd-16vcpu-64gb",
898 "so1_5-8vcpu-64gb",
899 "m-16vcpu-128gb",
900 "c-32",
901 "c2-32vcpu-64gb",
902 "m3-16vcpu-128gb",
903 "c-32-intel",
904 "c2-32vcpu-64gb-intel",
905 "c-48",
906 "m-24vcpu-192gb",
907 "g-32vcpu-128gb",
908 "so-16vcpu-128gb",
909 "m6-16vcpu-128gb",
910 "gd-32vcpu-128gb",
911 "c2-48vcpu-96gb",
912 "m3-24vcpu-192gb",
913 "g-40vcpu-160gb",
914 "so1_5-16vcpu-128gb",
915 "m-32vcpu-256gb",
916 "gd-40vcpu-160gb",
917 "so-24vcpu-192gb",
918 "m6-24vcpu-192gb",
919 "m3-32vcpu-256gb",
920 "so1_5-24vcpu-192gb",
921 "so-32vcpu-256gb",
922 "m6-32vcpu-256gb",
923 "so1_5-32vcpu-256gb"
924 ]
925 },
926 "tags": []
927 }
928 ],
929 "links": {
930 "pages": {
931 "first": "https://api.digitalocean.com/v2/droplets?page=1&per_page=1",
932 "prev": "https://api.digitalocean.com/v2/droplets?page=1&per_page=1"
933 }
934 },
935 "meta": {
936 "total": 104
937 }
938 }
939 "#;
940
941 let server = MockServer::start();
942 let list_droplets_page_one_mock = server.mock(|when, then| {
943 when.method(GET)
944 .path("/v2/droplets")
945 .query_param("page", "1")
946 .query_param("per_page", "1");
947 then.status(200)
948 .header("Content-Type", "application/json")
949 .body(MOCK_API_PAGE_1_RESPONSE);
950 });
951 let list_droplets_page_two_mock = server.mock(|when, then| {
952 when.method(GET)
953 .path("/v2/droplets")
954 .query_param("page", "2")
955 .query_param("per_page", "1");
956 then.status(200)
957 .header("Content-Type", "application/json")
958 .body(MOCK_API_PAGE_2_RESPONSE);
959 });
960
961 let client = DigitalOceanClient {
962 base_url: server.base_url(),
963 access_token: String::from("fake_token"),
964 page_size: 1,
965 };
966
967 let droplets = client.list_droplets(false).await?;
968 assert_eq!(2, droplets.len());
969 assert_eq!(118019015, droplets[0].id);
970 assert_eq!("testnet-node-01", droplets[0].name);
971 assert_eq!(
972 Ipv4Addr::from_str("104.248.0.110").unwrap(),
973 droplets[0].ip_address
974 );
975 assert_eq!(177884621, droplets[1].id);
976 assert_eq!("testnet-node-02", droplets[1].name);
977 assert_eq!(
978 Ipv4Addr::from_str("104.248.0.111").unwrap(),
979 droplets[1].ip_address
980 );
981
982 list_droplets_page_one_mock.assert();
983 list_droplets_page_two_mock.assert();
984
985 Ok(())
986 }
987
988 #[tokio::test]
989 async fn test_list_droplets_with_unauthorized_response() -> Result<()> {
990 const MOCK_API_RESPONSE: &str =
991 r#"{ "id": "Unauthorized", "message": "Unable to authenticate you" }"#;
992 let server = MockServer::start();
993 let list_droplets_page_one_mock = server.mock(|when, then| {
994 when.method(GET)
995 .path("/v2/droplets")
996 .query_param("page", "1")
997 .query_param("per_page", "1");
998 then.status(401)
999 .header("Content-Type", "application/json")
1000 .body(MOCK_API_RESPONSE);
1001 });
1002
1003 let client = DigitalOceanClient {
1004 base_url: server.base_url(),
1005 access_token: String::from("fake_token"),
1006 page_size: 1,
1007 };
1008
1009 let result = client.list_droplets(false).await;
1010 match result {
1011 Ok(_) => return Err(eyre!("This test should return an error")),
1012 Err(e) => {
1013 assert_eq!(
1014 e.to_string(),
1015 "Authorization failed for the Digital Ocean API"
1016 );
1017 }
1018 }
1019
1020 list_droplets_page_one_mock.assert();
1021
1022 Ok(())
1023 }
1024
1025 #[tokio::test]
1026 async fn test_list_droplets_with_unexpected_response() -> Result<()> {
1027 const MOCK_API_RESPONSE: &str =
1028 r#"{ "id": "unexpected", "message": "Something unexpected happened" }"#;
1029 let server = MockServer::start();
1030 let list_droplets_page_one_mock = server.mock(|when, then| {
1031 when.method(GET)
1032 .path("/v2/droplets")
1033 .query_param("page", "1")
1034 .query_param("per_page", "1");
1035 then.status(500)
1036 .header("Content-Type", "application/json")
1037 .body(MOCK_API_RESPONSE);
1038 });
1039
1040 let client = DigitalOceanClient {
1041 base_url: server.base_url(),
1042 access_token: String::from("fake_token"),
1043 page_size: 1,
1044 };
1045
1046 let result = client.list_droplets(false).await;
1047 match result {
1048 Ok(_) => return Err(eyre!("This test should return an error")),
1049 Err(e) => {
1050 assert_eq!(
1051 e.to_string(),
1052 "Unexpected response: 500 -- { \"id\": \"unexpected\", \"message\": \"Something unexpected happened\" }"
1053 );
1054 }
1055 }
1056
1057 list_droplets_page_one_mock.assert();
1058
1059 Ok(())
1060 }
1061
1062 #[tokio::test]
1063 async fn test_list_droplets_when_response_has_varying_ip_addresses() -> Result<()> {
1064 const MOCK_API_RESPONSE: &str = r#"
1067 {
1068 "droplets": [
1069 {
1070 "id": 118019015,
1071 "name": "testnet-node-01",
1072 "memory": 2048,
1073 "vcpus": 1,
1074 "disk": 50,
1075 "locked": false,
1076 "status": "active",
1077 "kernel": null,
1078 "created_at": "2018-11-05T13:57:25Z",
1079 "features": [],
1080 "backup_ids": [],
1081 "next_backup_window": null,
1082 "snapshot_ids": [
1083 136060266
1084 ],
1085 "image": {
1086 "id": 33346667,
1087 "name": "Droplet Deployer OLD",
1088 "distribution": "Ubuntu",
1089 "slug": null,
1090 "public": false,
1091 "regions": [
1092 "lon1",
1093 "nyc1",
1094 "sfo1",
1095 "ams2",
1096 "sgp1",
1097 "fra1",
1098 "tor1",
1099 "blr1"
1100 ],
1101 "created_at": "2018-04-10T10:40:49Z",
1102 "min_disk_size": 50,
1103 "type": "snapshot",
1104 "size_gigabytes": 7.11,
1105 "tags": [],
1106 "status": "available"
1107 },
1108 "volume_ids": [],
1109 "size": {
1110 "slug": "s-1vcpu-2gb",
1111 "memory": 2048,
1112 "vcpus": 1,
1113 "disk": 50,
1114 "transfer": 2,
1115 "price_monthly": 12,
1116 "price_hourly": 0.01786,
1117 "regions": [
1118 "ams3",
1119 "blr1",
1120 "fra1",
1121 "lon1",
1122 "nyc1",
1123 "nyc3",
1124 "sfo2",
1125 "sfo3",
1126 "sgp1",
1127 "syd1",
1128 "tor1"
1129 ],
1130 "available": true,
1131 "description": "Basic"
1132 },
1133 "size_slug": "s-1vcpu-2gb",
1134 "networks": {
1135 "v4": [
1136 {
1137 "ip_address": "192.168.0.2",
1138 "netmask": "255.255.240.0",
1139 "gateway": "192.168.0.1",
1140 "type": "public"
1141 }
1142 ],
1143 "v6": []
1144 },
1145 "region": {
1146 "name": "London 1",
1147 "slug": "lon1",
1148 "features": [
1149 "backups",
1150 "ipv6",
1151 "metadata",
1152 "install_agent",
1153 "storage",
1154 "image_transfer"
1155 ],
1156 "available": true,
1157 "sizes": [
1158 "s-1vcpu-1gb",
1159 "s-1vcpu-1gb-amd",
1160 "s-1vcpu-1gb-intel",
1161 "s-1vcpu-1gb-35gb-intel",
1162 "s-1vcpu-2gb",
1163 "s-1vcpu-2gb-amd",
1164 "s-1vcpu-2gb-intel",
1165 "s-1vcpu-2gb-70gb-intel",
1166 "s-2vcpu-2gb",
1167 "s-2vcpu-2gb-amd",
1168 "s-2vcpu-2gb-intel",
1169 "s-2vcpu-2gb-90gb-intel",
1170 "s-2vcpu-4gb",
1171 "s-2vcpu-4gb-amd",
1172 "s-2vcpu-4gb-intel",
1173 "s-2vcpu-4gb-120gb-intel",
1174 "c-2",
1175 "c2-2vcpu-4gb",
1176 "s-4vcpu-8gb",
1177 "s-4vcpu-8gb-amd",
1178 "s-4vcpu-8gb-intel",
1179 "g-2vcpu-8gb",
1180 "s-4vcpu-8gb-240gb-intel",
1181 "gd-2vcpu-8gb",
1182 "m-2vcpu-16gb",
1183 "c-4",
1184 "c2-4vcpu-8gb",
1185 "s-8vcpu-16gb",
1186 "m3-2vcpu-16gb",
1187 "s-8vcpu-16gb-amd",
1188 "s-8vcpu-16gb-intel",
1189 "g-4vcpu-16gb",
1190 "s-8vcpu-16gb-480gb-intel",
1191 "so-2vcpu-16gb",
1192 "m6-2vcpu-16gb",
1193 "gd-4vcpu-16gb",
1194 "so1_5-2vcpu-16gb",
1195 "m-4vcpu-32gb",
1196 "c-8",
1197 "c2-8vcpu-16gb",
1198 "m3-4vcpu-32gb",
1199 "g-8vcpu-32gb",
1200 "so-4vcpu-32gb",
1201 "m6-4vcpu-32gb",
1202 "gd-8vcpu-32gb",
1203 "so1_5-4vcpu-32gb",
1204 "m-8vcpu-64gb",
1205 "c-16",
1206 "c2-16vcpu-32gb",
1207 "m3-8vcpu-64gb",
1208 "g-16vcpu-64gb",
1209 "so-8vcpu-64gb",
1210 "m6-8vcpu-64gb",
1211 "gd-16vcpu-64gb",
1212 "so1_5-8vcpu-64gb",
1213 "m-16vcpu-128gb",
1214 "c-32",
1215 "c2-32vcpu-64gb",
1216 "m3-16vcpu-128gb",
1217 "c-48",
1218 "m-24vcpu-192gb",
1219 "g-32vcpu-128gb",
1220 "so-16vcpu-128gb",
1221 "m6-16vcpu-128gb",
1222 "gd-32vcpu-128gb",
1223 "c2-48vcpu-96gb",
1224 "m3-24vcpu-192gb",
1225 "g-40vcpu-160gb",
1226 "so1_5-16vcpu-128gb",
1227 "m-32vcpu-256gb",
1228 "gd-40vcpu-160gb",
1229 "so-24vcpu-192gb",
1230 "m6-24vcpu-192gb",
1231 "m3-32vcpu-256gb",
1232 "so1_5-24vcpu-192gb",
1233 "so-32vcpu-256gb",
1234 "m6-32vcpu-256gb",
1235 "so1_5-32vcpu-256gb"
1236 ]
1237 },
1238 "tags": []
1239 },
1240 {
1241 "id": 177884621,
1242 "name": "testnet-node-02",
1243 "memory": 2048,
1244 "vcpus": 2,
1245 "disk": 60,
1246 "locked": false,
1247 "status": "active",
1248 "kernel": null,
1249 "created_at": "2020-01-30T03:39:42Z",
1250 "features": [],
1251 "backup_ids": [],
1252 "next_backup_window": null,
1253 "snapshot_ids": [
1254 136060570
1255 ],
1256 "image": {
1257 "id": 53893572,
1258 "name": "18.04.3 (LTS) x64",
1259 "distribution": "Ubuntu",
1260 "slug": null,
1261 "public": false,
1262 "regions": [],
1263 "created_at": "2019-10-22T01:38:19Z",
1264 "min_disk_size": 20,
1265 "type": "base",
1266 "size_gigabytes": 2.36,
1267 "description": "Ubuntu 18.04 x64 20191022",
1268 "tags": [],
1269 "status": "deleted"
1270 },
1271 "volume_ids": [],
1272 "size": {
1273 "slug": "s-2vcpu-2gb",
1274 "memory": 2048,
1275 "vcpus": 2,
1276 "disk": 60,
1277 "transfer": 3,
1278 "price_monthly": 18,
1279 "price_hourly": 0.02679,
1280 "regions": [
1281 "ams3",
1282 "blr1",
1283 "fra1",
1284 "lon1",
1285 "nyc1",
1286 "nyc3",
1287 "sfo2",
1288 "sfo3",
1289 "sgp1",
1290 "syd1",
1291 "tor1"
1292 ],
1293 "available": true,
1294 "description": "Basic"
1295 },
1296 "size_slug": "s-2vcpu-2gb",
1297 "networks": {
1298 "v4": [
1299 {
1300 "ip_address": "192.168.0.3",
1301 "netmask": "255.255.240.0",
1302 "gateway": "192.168.0.1",
1303 "type": "private"
1304 },
1305 {
1306 "ip_address": "104.248.0.111",
1307 "netmask": "255.255.240.0",
1308 "gateway": "192.168.0.1",
1309 "type": "public"
1310 }
1311 ],
1312 "v6": []
1313 },
1314 "region": {
1315 "name": "New York 3",
1316 "slug": "nyc3",
1317 "features": [
1318 "backups",
1319 "ipv6",
1320 "metadata",
1321 "install_agent",
1322 "storage",
1323 "image_transfer"
1324 ],
1325 "available": true,
1326 "sizes": [
1327 "s-1vcpu-1gb",
1328 "s-1vcpu-1gb-amd",
1329 "s-1vcpu-1gb-intel",
1330 "s-1vcpu-1gb-35gb-intel",
1331 "s-1vcpu-2gb",
1332 "s-1vcpu-2gb-amd",
1333 "s-1vcpu-2gb-intel",
1334 "s-1vcpu-2gb-70gb-intel",
1335 "s-2vcpu-2gb",
1336 "s-2vcpu-2gb-amd",
1337 "s-2vcpu-2gb-intel",
1338 "s-2vcpu-2gb-90gb-intel",
1339 "s-2vcpu-4gb",
1340 "s-2vcpu-4gb-amd",
1341 "s-2vcpu-4gb-intel",
1342 "s-2vcpu-4gb-120gb-intel",
1343 "s-2vcpu-8gb-amd",
1344 "c-2",
1345 "c2-2vcpu-4gb",
1346 "s-2vcpu-8gb-160gb-intel",
1347 "s-4vcpu-8gb",
1348 "s-4vcpu-8gb-amd",
1349 "s-4vcpu-8gb-intel",
1350 "g-2vcpu-8gb",
1351 "s-4vcpu-8gb-240gb-intel",
1352 "gd-2vcpu-8gb",
1353 "s-4vcpu-16gb-amd",
1354 "m-2vcpu-16gb",
1355 "c-4",
1356 "c2-4vcpu-8gb",
1357 "s-4vcpu-16gb-320gb-intel",
1358 "s-8vcpu-16gb",
1359 "m3-2vcpu-16gb",
1360 "c-4-intel",
1361 "s-8vcpu-16gb-amd",
1362 "s-8vcpu-16gb-intel",
1363 "c2-4vcpu-8gb-intel",
1364 "g-4vcpu-16gb",
1365 "s-8vcpu-16gb-480gb-intel",
1366 "so-2vcpu-16gb",
1367 "m6-2vcpu-16gb",
1368 "gd-4vcpu-16gb",
1369 "so1_5-2vcpu-16gb",
1370 "s-8vcpu-32gb-amd",
1371 "m-4vcpu-32gb",
1372 "c-8",
1373 "c2-8vcpu-16gb",
1374 "s-8vcpu-32gb-640gb-intel",
1375 "m3-4vcpu-32gb",
1376 "c-8-intel",
1377 "c2-8vcpu-16gb-intel",
1378 "g-8vcpu-32gb",
1379 "so-4vcpu-32gb",
1380 "m6-4vcpu-32gb",
1381 "gd-8vcpu-32gb",
1382 "so1_5-4vcpu-32gb",
1383 "s-16vcpu-64gb-amd",
1384 "m-8vcpu-64gb",
1385 "c-16",
1386 "c2-16vcpu-32gb",
1387 "s-16vcpu-64gb-intel",
1388 "m3-8vcpu-64gb",
1389 "c-16-intel",
1390 "c2-16vcpu-32gb-intel",
1391 "g-16vcpu-64gb",
1392 "so-8vcpu-64gb",
1393 "m6-8vcpu-64gb",
1394 "gd-16vcpu-64gb",
1395 "so1_5-8vcpu-64gb",
1396 "m-16vcpu-128gb",
1397 "c-32",
1398 "c2-32vcpu-64gb",
1399 "m3-16vcpu-128gb",
1400 "c-32-intel",
1401 "c2-32vcpu-64gb-intel",
1402 "c-48",
1403 "m-24vcpu-192gb",
1404 "g-32vcpu-128gb",
1405 "so-16vcpu-128gb",
1406 "m6-16vcpu-128gb",
1407 "gd-32vcpu-128gb",
1408 "c2-48vcpu-96gb",
1409 "m3-24vcpu-192gb",
1410 "g-40vcpu-160gb",
1411 "so1_5-16vcpu-128gb",
1412 "m-32vcpu-256gb",
1413 "gd-40vcpu-160gb",
1414 "so-24vcpu-192gb",
1415 "m6-24vcpu-192gb",
1416 "m3-32vcpu-256gb",
1417 "so1_5-24vcpu-192gb",
1418 "so-32vcpu-256gb",
1419 "m6-32vcpu-256gb",
1420 "so1_5-32vcpu-256gb"
1421 ]
1422 },
1423 "tags": []
1424 }
1425 ],
1426 "links": {},
1427 "meta": {
1428 "total": 104
1429 }
1430 }
1431 "#;
1432
1433 let server = MockServer::start();
1434 let list_droplets_mock = server.mock(|when, then| {
1435 when.method(GET).path("/v2/droplets");
1436 then.status(200)
1437 .header("Content-Type", "application/json")
1438 .body(MOCK_API_RESPONSE);
1439 });
1440
1441 let client = DigitalOceanClient {
1442 base_url: server.base_url(),
1443 access_token: String::from("fake_token"),
1444 page_size: DIGITAL_OCEAN_API_PAGE_SIZE,
1445 };
1446
1447 let droplets = client.list_droplets(false).await?;
1448
1449 assert_eq!(2, droplets.len());
1450 assert_eq!(118019015, droplets[0].id);
1451 assert_eq!("testnet-node-01", droplets[0].name);
1452 assert_eq!(
1453 Ipv4Addr::from_str("192.168.0.2").unwrap(),
1454 droplets[0].ip_address
1455 );
1456 assert_eq!(177884621, droplets[1].id);
1457 assert_eq!("testnet-node-02", droplets[1].name);
1458 assert_eq!(
1459 Ipv4Addr::from_str("104.248.0.111").unwrap(),
1460 droplets[1].ip_address
1461 );
1462
1463 list_droplets_mock.assert();
1464
1465 Ok(())
1466 }
1467}