Skip to main content

icann_rdap_cli/dirs/
fcbs.rs

1use std::{
2    fs::{self, File},
3    io::{BufRead, BufReader, Read},
4    path::PathBuf,
5};
6
7use crate::dirs::config_dir;
8
9use {
10    icann_rdap_client::iana::{BootstrapStore, RegistryHasNotExpired},
11    icann_rdap_common::{
12        httpdata::HttpData,
13        iana::{BootstrapRegistry, IanaRegistry, IanaRegistryType},
14    },
15    tracing::debug,
16};
17
18use super::bootstrap_cache_path;
19
20pub struct FileCacheBootstrapStore;
21
22impl BootstrapStore for FileCacheBootstrapStore {
23    fn has_bootstrap_registry(
24        &self,
25        reg_type: &IanaRegistryType,
26    ) -> Result<bool, icann_rdap_client::RdapClientError> {
27        let file_name = reg_type.file_name();
28        let path = bootstrap_cache_path().join(file_name);
29        if path.exists() {
30            debug!("Looking for {file_name} bootstrap information.");
31            let fc_reg = read_bootstrap_cache_file(path, |_| {})?;
32            return Ok(Some(fc_reg).registry_has_not_expired());
33        }
34        Ok(false)
35    }
36
37    fn put_bootstrap_registry(
38        &self,
39        reg_type: &IanaRegistryType,
40        registry: IanaRegistry,
41        http_data: HttpData,
42    ) -> Result<(), icann_rdap_client::RdapClientError> {
43        let bootstrap_cache_path = bootstrap_cache_path().join(reg_type.file_name());
44        let data = serde_json::to_string_pretty(&registry)?;
45        let cache_contents = http_data.to_lines(&data)?;
46        fs::write(bootstrap_cache_path, cache_contents)?;
47        Ok(())
48    }
49
50    fn get_dns_urls(
51        &self,
52        ldh: &str,
53    ) -> Result<Option<Vec<String>>, icann_rdap_client::RdapClientError> {
54        self.get_bootstrap_urls(&IanaRegistryType::RdapBootstrapDns, ldh)
55    }
56
57    fn get_asn_urls(
58        &self,
59        asn: &str,
60    ) -> Result<Option<Vec<String>>, icann_rdap_client::RdapClientError> {
61        self.get_bootstrap_urls(&IanaRegistryType::RdapBootstrapAsn, asn)
62    }
63
64    fn get_ipv4_urls(
65        &self,
66        ipv4: &str,
67    ) -> Result<Option<Vec<String>>, icann_rdap_client::RdapClientError> {
68        self.get_bootstrap_urls(&IanaRegistryType::RdapBootstrapIpv4, ipv4)
69    }
70
71    fn get_ipv6_urls(
72        &self,
73        ipv6: &str,
74    ) -> Result<Option<Vec<String>>, icann_rdap_client::RdapClientError> {
75        self.get_bootstrap_urls(&IanaRegistryType::RdapBootstrapIpv6, ipv6)
76    }
77
78    fn get_tag_urls(
79        &self,
80        tag: &str,
81    ) -> Result<Option<Vec<String>>, icann_rdap_client::RdapClientError> {
82        self.get_bootstrap_urls(&IanaRegistryType::RdapObjectTags, tag)
83    }
84}
85
86impl FileCacheBootstrapStore {
87    fn get_bootstrap_urls(
88        &self,
89        reg_type: &IanaRegistryType,
90        key: &str,
91    ) -> Result<Option<Vec<String>>, icann_rdap_client::RdapClientError> {
92        let file_name = reg_type.file_name();
93
94        // Check in configured bootstrap override
95        let config_bootstrap_path = config_dir().join(file_name);
96        if config_bootstrap_path.exists() {
97            let iana = read_bootstrap_config_file(config_bootstrap_path, |s| debug!("Reading {s}"));
98            match iana {
99                Ok(iana) => {
100                    let urls = match reg_type {
101                        IanaRegistryType::RdapBootstrapDns => iana.get_dns_bootstrap_urls(key),
102                        IanaRegistryType::RdapBootstrapAsn => iana.get_asn_bootstrap_urls(key),
103                        IanaRegistryType::RdapBootstrapIpv4 => iana.get_ipv4_bootstrap_urls(key),
104                        IanaRegistryType::RdapBootstrapIpv6 => iana.get_ipv6_bootstrap_urls(key),
105                        IanaRegistryType::RdapObjectTags => iana.get_tag_bootstrap_urls(key),
106                    };
107                    match urls {
108                        Ok(Some(urls)) => {
109                            debug!("Bootstrap URLs found in configured bootstrap override.");
110                            return Ok(Some(urls));
111                        }
112                        Ok(None) => {}
113                        Err(e) => return Err(e.into()),
114                    }
115                }
116                Err(err) => return Err(err),
117            }
118        }
119
120        // Fall back to bootstrap cache
121        let bootstrap_cache_path = bootstrap_cache_path().join(file_name);
122        let (iana, _http_data) =
123            read_bootstrap_cache_file(bootstrap_cache_path, |s| debug!("Reading {s}"))?;
124        let urls = match reg_type {
125            IanaRegistryType::RdapBootstrapDns => iana.get_dns_bootstrap_urls(key),
126            IanaRegistryType::RdapBootstrapAsn => iana.get_asn_bootstrap_urls(key),
127            IanaRegistryType::RdapBootstrapIpv4 => iana.get_ipv4_bootstrap_urls(key),
128            IanaRegistryType::RdapBootstrapIpv6 => iana.get_ipv6_bootstrap_urls(key),
129            IanaRegistryType::RdapObjectTags => iana.get_tag_bootstrap_urls(key),
130        };
131        Ok(urls?)
132    }
133}
134
135pub fn read_bootstrap_cache_file<F>(
136    path: PathBuf,
137    callback: F,
138) -> Result<(IanaRegistry, HttpData), std::io::Error>
139where
140    F: FnOnce(String),
141{
142    let input = File::open(&path)?;
143    let buf = BufReader::new(input);
144    let mut lines = vec![];
145    for line in buf.lines() {
146        lines.push(line?);
147    }
148    let cache_data = HttpData::from_lines(&lines)?;
149    callback(path.display().to_string());
150    let iana: IanaRegistry = serde_json::from_str(&cache_data.1.join(""))?;
151    Ok((iana, cache_data.0))
152}
153
154pub fn read_bootstrap_config_file<F>(
155    path: PathBuf,
156    callback: F,
157) -> Result<IanaRegistry, icann_rdap_client::RdapClientError>
158where
159    F: FnOnce(String),
160{
161    let mut input = File::open(&path)?;
162    let mut content = String::new();
163    input.read_to_string(&mut content)?;
164    callback(path.display().to_string());
165    let iana: IanaRegistry = serde_json::from_str(&content)?;
166    Ok(iana)
167}
168
169#[cfg(test)]
170mod test {
171    use {
172        icann_rdap_client::{
173            iana::{BootstrapStore, PreferredUrl},
174            rdap::QueryType,
175        },
176        icann_rdap_common::{
177            httpdata::HttpData,
178            iana::{BootstrapRegistry, IanaRegistry, IanaRegistryType},
179        },
180        serial_test::serial,
181        test_dir::{DirBuilder, FileType, TestDir},
182    };
183
184    use crate::dirs::{self, bootstrap_cache_path, config_dir, fcbs::FileCacheBootstrapStore};
185
186    fn test_dir() -> TestDir {
187        let test_dir = TestDir::temp()
188            .create("cache", FileType::Dir)
189            .create("config", FileType::Dir);
190        unsafe {
191            std::env::set_var("XDG_CACHE_HOME", test_dir.path("cache"));
192            std::env::set_var("XDG_CONFIG_HOME", test_dir.path("config"));
193        };
194        dirs::init().expect("unable to init directories");
195        test_dir
196    }
197
198    #[test]
199    #[serial]
200    fn test_fcbootstrap_with_dns() {
201        // GIVEN
202        let _test_dir = test_dir();
203        let bs = FileCacheBootstrapStore;
204        let bootstrap = r#"
205            {
206                "version": "1.0",
207                "publication": "2024-01-07T10:11:12Z",
208                "description": "Some text",
209                "services": [
210                  [
211                    ["net", "com"],
212                    [
213                      "https://registry.example.com/myrdap/"
214                    ]
215                  ],
216                  [
217                    ["org", "mytld"],
218                    [
219                      "https://example.org/"
220                    ]
221                  ]
222                ]
223            }
224        "#;
225        let iana =
226            serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse domain bootstrap");
227        bs.put_bootstrap_registry(
228            &IanaRegistryType::RdapBootstrapDns,
229            iana,
230            HttpData::example().build(),
231        )
232        .expect("put iana registry");
233
234        // WHEN
235        let actual = bs
236            .get_domain_query_urls(&QueryType::domain("example.org").expect("invalid domain name"))
237            .expect("get bootstrap url")
238            .preferred_url()
239            .expect("preferred url");
240
241        // THEN
242        assert_eq!(actual, "https://example.org/")
243    }
244
245    #[test]
246    #[serial]
247    fn test_fcbootstrap_with_autnum() {
248        // GIVEN
249        let _test_dir = test_dir();
250        let bs = FileCacheBootstrapStore;
251        let bootstrap = r#"
252            {
253                "version": "1.0",
254                "publication": "2024-01-07T10:11:12Z",
255                "description": "RDAP Bootstrap file for example registries.",
256                "services": [
257                  [
258                    ["64496-64496"],
259                    [
260                      "https://rir3.example.com/myrdap/"
261                    ]
262                  ],
263                  [
264                    ["64497-64510", "65536-65551"],
265                    [
266                      "https://example.org/"
267                    ]
268                  ],
269                  [
270                    ["64512-65534"],
271                    [
272                      "http://example.net/rdaprir2/",
273                      "https://example.net/rdaprir2/"
274                    ]
275                  ]
276                ]
277            }
278        "#;
279        let iana =
280            serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse autnum bootstrap");
281        bs.put_bootstrap_registry(
282            &IanaRegistryType::RdapBootstrapAsn,
283            iana,
284            HttpData::example().build(),
285        )
286        .expect("put iana registry");
287
288        // WHEN
289        let actual = bs
290            .get_autnum_query_urls(&QueryType::autnum("as64512").expect("invalid autnum"))
291            .expect("get bootstrap url")
292            .preferred_url()
293            .expect("preferred url");
294
295        // THEN
296        assert_eq!(actual, "https://example.net/rdaprir2/");
297    }
298
299    #[test]
300    #[serial]
301    fn test_fcbootstrap_with_ipv4() {
302        // GIVEN
303        let _test_dir = test_dir();
304        let bs = FileCacheBootstrapStore;
305        let bootstrap = r#"
306            {
307                "version": "1.0",
308                "publication": "2024-01-07T10:11:12Z",
309                "description": "RDAP Bootstrap file for example registries.",
310                "services": [
311                  [
312                    ["198.51.100.0/24", "192.0.0.0/8"],
313                    [
314                      "https://rir1.example.com/myrdap/"
315                    ]
316                  ],
317                  [
318                    ["203.0.113.0/24", "192.0.2.0/24"],
319                    [
320                      "https://example.org/"
321                    ]
322                  ],
323                  [
324                    ["203.0.113.0/28"],
325                    [
326                      "https://example.net/rdaprir2/",
327                      "http://example.net/rdaprir2/"
328                    ]
329                  ]
330                ]
331            }
332        "#;
333        let iana =
334            serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse autnum bootstrap");
335        bs.put_bootstrap_registry(
336            &IanaRegistryType::RdapBootstrapIpv4,
337            iana,
338            HttpData::example().build(),
339        )
340        .expect("put iana registry");
341
342        // WHEN
343        let actual = bs
344            .get_ipv4_query_urls(&QueryType::ipv4("198.51.100.1").expect("invalid IP address"))
345            .expect("get bootstrap url")
346            .preferred_url()
347            .expect("preferred url");
348
349        // THEN
350        assert_eq!(actual, "https://rir1.example.com/myrdap/");
351    }
352
353    #[test]
354    #[serial]
355    fn test_fcbootstrap_with_ipv6() {
356        // GIVEN
357        let _test_dir = test_dir();
358        let bs = FileCacheBootstrapStore;
359        let bootstrap = r#"
360            {
361                "version": "1.0",
362                "publication": "2024-01-07T10:11:12Z",
363                "description": "RDAP Bootstrap file for example registries.",
364                "services": [
365                  [
366                    ["2001:db8::/34"],
367                    [
368                      "https://rir2.example.com/myrdap/"
369                    ]
370                  ],
371                  [
372                    ["2001:db8:4000::/36", "2001:db8:ffff::/48"],
373                    [
374                      "https://example.org/"
375                    ]
376                  ],
377                  [
378                    ["2001:db8:1000::/36"],
379                    [
380                      "https://example.net/rdaprir2/",
381                      "http://example.net/rdaprir2/"
382                    ]
383                  ]
384                ]
385            }
386        "#;
387        let iana =
388            serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse autnum bootstrap");
389        bs.put_bootstrap_registry(
390            &IanaRegistryType::RdapBootstrapIpv6,
391            iana,
392            HttpData::example().build(),
393        )
394        .expect("put iana registry");
395
396        // WHEN
397        let actual = bs
398            .get_ipv6_query_urls(&QueryType::ipv6("2001:db8::1").expect("invalid IP address"))
399            .expect("get bootstrap url")
400            .preferred_url()
401            .expect("preferred url");
402
403        // THEN
404        assert_eq!(actual, "https://rir2.example.com/myrdap/");
405    }
406
407    #[test]
408    #[serial]
409    fn test_fcbootstrap_with_tag() {
410        // GIVEN
411        let _test_dir = test_dir();
412        let bs = FileCacheBootstrapStore;
413        let bootstrap = r#"
414            {
415              "version": "1.0",
416              "publication": "YYYY-MM-DDTHH:MM:SSZ",
417              "description": "RDAP bootstrap file for service provider object tags",
418              "services": [
419                [
420                  ["contact@example.com"],
421                  ["YYYY"],
422                  [
423                    "https://example.com/rdap/"
424                  ]
425                ],
426                [
427                  ["contact@example.org"],
428                  ["ZZ54"],
429                  [
430                    "http://rdap.example.org/"
431                  ]
432                ],
433                [
434                  ["contact@example.net"],
435                  ["1754"],
436                  [
437                    "https://example.net/rdap/",
438                    "http://example.net/rdap/"
439                  ]
440                ]
441              ]
442             }
443        "#;
444        let iana =
445            serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse autnum bootstrap");
446        bs.put_bootstrap_registry(
447            &IanaRegistryType::RdapObjectTags,
448            iana,
449            HttpData::example().build(),
450        )
451        .expect("put iana registry");
452
453        // WHEN
454        let actual = bs
455            .get_entity_handle_query_urls(&QueryType::Entity("foo-YYYY".to_string()))
456            .expect("get bootstrap url")
457            .preferred_url()
458            .expect("preferred url");
459
460        // THEN
461        assert_eq!(actual, "https://example.com/rdap/")
462    }
463
464    #[test]
465    #[serial]
466    fn test_fcbootstrap_dns_config_override() {
467        // GIVEN
468        let _test_dir = test_dir();
469        let bs = FileCacheBootstrapStore;
470
471        let cache_bootstrap = r#"
472            {
473                "version": "1.0",
474                "publication": "2024-01-07T10:11:12Z",
475                "description": "Cache bootstrap",
476                "services": [
477                  [
478                    ["org"],
479                    [
480                      "https://cache.example.org/"
481                    ]
482                  ]
483                ]
484            }
485        "#;
486        let iana = serde_json::from_str::<IanaRegistry>(cache_bootstrap)
487            .expect("cannot parse cache bootstrap");
488        bs.put_bootstrap_registry(
489            &IanaRegistryType::RdapBootstrapDns,
490            iana,
491            HttpData::example().build(),
492        )
493        .expect("put cache iana registry");
494
495        let config_bootstrap = r#"
496            {
497                "version": "1.0",
498                "publication": "2024-01-07T10:11:12Z",
499                "description": "Config override bootstrap",
500                "services": [
501                  [
502                    ["org"],
503                    [
504                      "https://config.example.org/"
505                    ]
506                  ]
507                ]
508            }
509        "#;
510        let bootstrap_config_path = config_dir().join("dns.json");
511        std::fs::write(&bootstrap_config_path, config_bootstrap).expect("write config bootstrap");
512
513        // WHEN
514        let actual = bs
515            .get_domain_query_urls(&QueryType::domain("example.org").expect("invalid domain name"))
516            .expect("get bootstrap url")
517            .preferred_url()
518            .expect("preferred url");
519
520        // THEN
521        assert_eq!(actual, "https://config.example.org/")
522    }
523
524    #[test]
525    #[serial]
526    fn test_fcbootstrap_asn_config_override() {
527        // GIVEN
528        let _test_dir = test_dir();
529        let bs = FileCacheBootstrapStore;
530
531        let cache_bootstrap = r#"
532            {
533                "version": "1.0",
534                "publication": "2024-01-07T10:11:12Z",
535                "description": "Cache bootstrap",
536                "services": [
537                  [
538                    ["64496-64496"],
539                    [
540                      "https://cache.example.com/"
541                    ]
542                  ]
543                ]
544            }
545        "#;
546        let iana = serde_json::from_str::<IanaRegistry>(cache_bootstrap)
547            .expect("cannot parse cache bootstrap");
548        bs.put_bootstrap_registry(
549            &IanaRegistryType::RdapBootstrapAsn,
550            iana,
551            HttpData::example().build(),
552        )
553        .expect("put cache iana registry");
554
555        let config_bootstrap = r#"
556            {
557                "version": "1.0",
558                "publication": "2024-01-07T10:11:12Z",
559                "description": "Config override bootstrap",
560                "services": [
561                  [
562                    ["64496-64496"],
563                    [
564                      "https://config.example.com/"
565                    ]
566                  ]
567                ]
568            }
569        "#;
570        let bootstrap_config_path = config_dir().join("asn.json");
571        std::fs::write(&bootstrap_config_path, config_bootstrap).expect("write config bootstrap");
572
573        // WHEN
574        let actual = bs
575            .get_autnum_query_urls(&QueryType::autnum("as64496").expect("invalid autnum"))
576            .expect("get bootstrap url")
577            .preferred_url()
578            .expect("preferred url");
579
580        // THEN
581        assert_eq!(actual, "https://config.example.com/")
582    }
583
584    #[test]
585    #[serial]
586    fn test_fcbootstrap_ipv4_config_override() {
587        // GIVEN
588        let _test_dir = test_dir();
589        let bs = FileCacheBootstrapStore;
590
591        let cache_bootstrap = r#"
592            {
593                "version": "1.0",
594                "publication": "2024-01-07T10:11:12Z",
595                "description": "Cache bootstrap",
596                "services": [
597                  [
598                    ["198.51.100.0/24"],
599                    [
600                      "https://cache.example.com/"
601                    ]
602                  ]
603                ]
604            }
605        "#;
606        let iana = serde_json::from_str::<IanaRegistry>(cache_bootstrap)
607            .expect("cannot parse cache bootstrap");
608        bs.put_bootstrap_registry(
609            &IanaRegistryType::RdapBootstrapIpv4,
610            iana,
611            HttpData::example().build(),
612        )
613        .expect("put cache iana registry");
614
615        let config_bootstrap = r#"
616            {
617                "version": "1.0",
618                "publication": "2024-01-07T10:11:12Z",
619                "description": "Config override bootstrap",
620                "services": [
621                  [
622                    ["198.51.100.0/24"],
623                    [
624                      "https://config.example.com/"
625                    ]
626                  ]
627                ]
628            }
629        "#;
630        let bootstrap_config_path = config_dir().join("ipv4.json");
631        std::fs::write(&bootstrap_config_path, config_bootstrap).expect("write config bootstrap");
632
633        // WHEN
634        let actual = bs
635            .get_ipv4_query_urls(&QueryType::ipv4("198.51.100.1").expect("invalid IP address"))
636            .expect("get bootstrap url")
637            .preferred_url()
638            .expect("preferred url");
639
640        // THEN
641        assert_eq!(actual, "https://config.example.com/")
642    }
643
644    #[test]
645    #[serial]
646    fn test_fcbootstrap_ipv6_config_override() {
647        // GIVEN
648        let _test_dir = test_dir();
649        let bs = FileCacheBootstrapStore;
650
651        let cache_bootstrap = r#"
652            {
653                "version": "1.0",
654                "publication": "2024-01-07T10:11:12Z",
655                "description": "Cache bootstrap",
656                "services": [
657                  [
658                    ["2001:db8::/32"],
659                    [
660                      "https://cache.example.com/"
661                    ]
662                  ]
663                ]
664            }
665        "#;
666        let iana = serde_json::from_str::<IanaRegistry>(cache_bootstrap)
667            .expect("cannot parse cache bootstrap");
668        bs.put_bootstrap_registry(
669            &IanaRegistryType::RdapBootstrapIpv6,
670            iana,
671            HttpData::example().build(),
672        )
673        .expect("put cache iana registry");
674
675        let config_bootstrap = r#"
676            {
677                "version": "1.0",
678                "publication": "2024-01-07T10:11:12Z",
679                "description": "Config override bootstrap",
680                "services": [
681                  [
682                    ["2001:db8::/32"],
683                    [
684                      "https://config.example.com/"
685                    ]
686                  ]
687                ]
688            }
689        "#;
690        let bootstrap_config_path = config_dir().join("ipv6.json");
691        std::fs::write(&bootstrap_config_path, config_bootstrap).expect("write config bootstrap");
692
693        // WHEN
694        let actual = bs
695            .get_ipv6_query_urls(&QueryType::ipv6("2001:db8::1").expect("invalid IP address"))
696            .expect("get bootstrap url")
697            .preferred_url()
698            .expect("preferred url");
699
700        // THEN
701        assert_eq!(actual, "https://config.example.com/")
702    }
703
704    #[test]
705    #[serial]
706    fn test_fcbootstrap_tag_config_override() {
707        // GIVEN
708        let _test_dir = test_dir();
709        let bs = FileCacheBootstrapStore;
710
711        let cache_bootstrap = r#"
712            {
713              "version": "1.0",
714              "publication": "YYYY-MM-DDTHH:MM:SSZ",
715              "description": "Cache bootstrap",
716              "services": [
717                [
718                  ["contact@example.com"],
719                  ["YYYY"],
720                  [
721                    "https://cache.example.com/rdap/"
722                  ]
723                ]
724              ]
725             }
726        "#;
727        let iana = serde_json::from_str::<IanaRegistry>(cache_bootstrap)
728            .expect("cannot parse cache bootstrap");
729        bs.put_bootstrap_registry(
730            &IanaRegistryType::RdapObjectTags,
731            iana,
732            HttpData::example().build(),
733        )
734        .expect("put cache iana registry");
735
736        let config_bootstrap = r#"
737            {
738              "version": "1.0",
739              "publication": "YYYY-MM-DDTHH:MM:SSZ",
740              "description": "Config override bootstrap",
741              "services": [
742                [
743                  ["contact@example.com"],
744                  ["YYYY"],
745                  [
746                    "https://config.example.com/rdap/"
747                  ]
748                ]
749              ]
750             }
751        "#;
752        let bootstrap_config_path = config_dir().join("object-tags.json");
753        std::fs::write(&bootstrap_config_path, config_bootstrap).expect("write config bootstrap");
754
755        // WHEN
756        let actual = bs
757            .get_entity_handle_query_urls(&QueryType::Entity("foo-YYYY".to_string()))
758            .expect("get bootstrap url")
759            .preferred_url()
760            .expect("preferred url");
761
762        // THEN
763        assert_eq!(actual, "https://config.example.com/rdap/")
764    }
765
766    #[test]
767    fn test_iana_registry_propagates_invalid_asn_input_error() {
768        // GIVEN - valid ASN bootstrap
769        let bootstrap = r#"
770            {
771                "version": "1.0",
772                "publication": "2024-01-07T10:11:12Z",
773                "description": "ASN bootstrap",
774                "services": [
775                  [
776                    ["64496-64496"],
777                    [
778                      "https://example.org/"
779                    ]
780                  ]
781                ]
782            }
783        "#;
784        let iana: IanaRegistry =
785            serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse bootstrap");
786
787        // WHEN - query with invalid (non-numeric) ASN input
788        let result = iana.get_asn_bootstrap_urls("notanumber");
789
790        // THEN - should return error for invalid input
791        assert!(
792            result.is_err(),
793            "Expected error for invalid ASN input but got: {:?}",
794            result
795        );
796    }
797
798    #[test]
799    #[serial]
800    fn test_fcbootstrap_propagates_invalid_asn_via_store() {
801        // GIVEN - valid ASN bootstrap in store
802        let _test_dir = test_dir();
803        let bs = FileCacheBootstrapStore;
804        let bootstrap = r#"
805            {
806                "version": "1.0",
807                "publication": "2024-01-07T10:11:12Z",
808                "description": "ASN bootstrap",
809                "services": [
810                  [
811                    ["64496-64496"],
812                    [
813                      "https://example.org/"
814                    ]
815                  ]
816                ]
817            }
818        "#;
819        let iana = serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse bootstrap");
820        bs.put_bootstrap_registry(
821            &IanaRegistryType::RdapBootstrapAsn,
822            iana,
823            HttpData::example().build(),
824        )
825        .expect("put iana registry");
826
827        // WHEN - query with invalid ASN (non-numeric)
828        let result = bs.get_asn_urls("notanumber");
829
830        // THEN - should propagate the error
831        assert!(
832            result.is_err(),
833            "Expected error for invalid ASN but got: {:?}",
834            result
835        );
836    }
837
838    #[test]
839    #[serial]
840    fn test_fcbootstrap_config_override_propagates_errors() {
841        // GIVEN - config override with valid bootstrap but query returns None
842        let _test_dir = test_dir();
843        let bs = FileCacheBootstrapStore;
844
845        // Valid bootstrap but doesn't have "example.org"
846        let config_bootstrap = r#"
847            {
848                "version": "1.0",
849                "publication": "2024-01-07T10:11:12Z",
850                "description": "Config bootstrap",
851                "services": [
852                  [
853                    ["com"],
854                    [
855                      "https://config.example.org/"
856                    ]
857                  ]
858                ]
859            }
860        "#;
861        let bootstrap_config_path = config_dir().join("dns.json");
862        std::fs::write(&bootstrap_config_path, config_bootstrap).expect("write config bootstrap");
863
864        // Also put a cache version that has a different TLD
865        let cache_bootstrap = r#"
866            {
867                "version": "1.0",
868                "publication": "2024-01-07T10:11:12Z",
869                "description": "Cache bootstrap",
870                "services": [
871                  [
872                    ["net"],
873                    [
874                      "https://cache.example.org/"
875                    ]
876                  ]
877                ]
878            }
879        "#;
880        let iana =
881            serde_json::from_str::<IanaRegistry>(cache_bootstrap).expect("cannot parse bootstrap");
882        bs.put_bootstrap_registry(
883            &IanaRegistryType::RdapBootstrapDns,
884            iana,
885            HttpData::example().build(),
886        )
887        .expect("put iana registry");
888
889        // WHEN - query for a TLD that doesn't exist in either config or cache
890        let result = bs
891            .get_domain_query_urls(&QueryType::domain("example.org").expect("invalid domain name"));
892
893        // THEN - should return Ok(None) for not found, not an error
894        assert!(result.is_ok(), "Expected Ok but got: {:?}", result);
895        assert!(result.unwrap().is_none(), "Expected None but got Some");
896    }
897
898    #[test]
899    #[serial]
900    fn test_fcbootstrap_propagates_invalid_json_error() {
901        // GIVEN - invalid JSON in config override
902        let _test_dir = test_dir();
903        let bs = FileCacheBootstrapStore;
904
905        // Write invalid (malformed) JSON
906        let invalid_json = r#"{ "version": "1.0", invalid json }"#;
907        let bootstrap_config_path = config_dir().join("dns.json");
908        std::fs::write(&bootstrap_config_path, invalid_json).expect("write invalid json");
909
910        // WHEN
911        let result = bs
912            .get_domain_query_urls(&QueryType::domain("example.org").expect("invalid domain name"));
913
914        // THEN - should propagate the JSON parse error
915        assert!(
916            result.is_err(),
917            "Expected error for invalid JSON but got: {:?}",
918            result
919        );
920    }
921
922    #[test]
923    #[serial]
924    fn test_fcbootstrap_propagates_invalid_json_in_cache_error() {
925        // GIVEN - invalid JSON in cache file
926        let _test_dir = test_dir();
927        let bs = FileCacheBootstrapStore;
928
929        // Write invalid (malformed) JSON to cache directly
930        let invalid_json = r#"{ "version": "1.0", bad json }"#;
931        let cache_path = bootstrap_cache_path().join("dns.json");
932        std::fs::write(&cache_path, invalid_json).expect("write invalid json to cache");
933
934        // WHEN
935        let result = bs
936            .get_domain_query_urls(&QueryType::domain("example.org").expect("invalid domain name"));
937
938        // THEN - should propagate the JSON parse error
939        assert!(
940            result.is_err(),
941            "Expected error for invalid JSON in cache but got: {:?}",
942            result
943        );
944    }
945}