sn_testnet_deploy/
digital_ocean.rs

1// Copyright (c) 2023, MaidSafe.
2// All rights reserved.
3//
4// This SAFE Network Software is licensed under the BSD-3-Clause license.
5// Please see the LICENSE file for more details.
6
7use 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                // The following might fail if we start multiple networks in parallel.
78                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                // All the data was returned on a single page.
111                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        // This respresents a real response from the Digital Ocean API (with names and IP addresses
137        // changed), as of September 2023.
138        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        // This respresents a real response from the Digital Ocean API (with names and IP addresses
1065        // changed), as of September 2023.
1066        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}