//! Bounded download and index fetch of the add-on registry, against a fake transport. mod addons_support; use addons_support::{entry, index_json, url}; use notescraft_engine::addons::{FakeTransport, File, INDEX_URL, IndexCache, Response, Transport, Ureq, download_file, fetch_index, split_code}; use notescraft_syntax::validate::sha256_hex; const BODY: &[u8] = b"hello grammar"; const CAP: u64 = 512 * 1024; fn file(link: &str) -> File { File { path: "a.txt".into(), size: BODY.len() as u64, sha256: sha256_hex(BODY), url: link.into() } } fn good() -> String { url("grammar", "x", "1.0.0", "a.txt") } fn refused(link: &str) -> String { let transport = FakeTransport::new().with(link, BODY); download_file(&transport, &file(link), CAP).expect_err("refused").to_string() } #[test] fn a_listed_file_downloads() { let transport = FakeTransport::new().with(&good(), BODY); assert_eq!(download_file(&transport, &file(&good()), CAP).unwrap(), BODY); } #[test] fn hostile_links_never_reach_the_transport() { for link in [ "https://evil.example.com/a.txt".to_owned(), good().replacen("https", "http", 1), good().replacen("https://", "https://user:pw@", 1), good().replacen("git.marcospaulo.dev.br", "127.0.0.1", 1), format!("{}?x=1", good()), ] { let transport = FakeTransport::new().with(&link, BODY); assert!(download_file(&transport, &file(&link), CAP).is_err(), "{link}"); assert!(transport.asked().is_empty(), "{link} was fetched"); } assert!(refused("https://evil.example.com/a.txt").contains("registry")); } #[test] fn a_redirect_status_is_a_failure() { let transport = FakeTransport::new().with_response(&good(), Ok(Response { status: 302, etag: None, bytes: Vec::new() })); let error = download_file(&transport, &file(&good()), CAP).expect_err("redirect").to_string(); assert!(error.contains("download_failed"), "{error}"); } #[test] fn a_body_one_byte_too_long_or_short_is_refused() { let mut longer = BODY.to_vec(); longer.push(b'!'); let transport = FakeTransport::new().with(&good(), &longer); assert!(download_file(&transport, &file(&good()), CAP).is_err()); let transport = FakeTransport::new().with(&good(), &BODY[..BODY.len() - 1]); assert!(download_file(&transport, &file(&good()), CAP).is_err()); } #[test] fn a_wrong_hash_or_a_size_over_the_cap_is_refused() { let transport = FakeTransport::new().with(&good(), b"HELLO grammar"); assert!(download_file(&transport, &file(&good()), CAP).unwrap_err().to_string().contains("sha256")); let transport = FakeTransport::new().with(&good(), BODY); assert!(download_file(&transport, &file(&good()), 3).unwrap_err().to_string().contains("cap")); } #[test] fn no_network_maps_to_the_offline_code() { let error = download_file(&FakeTransport::new(), &file(&good()), CAP).unwrap_err().to_string(); let message = error.strip_prefix("add-on: ").expect("an add-on error"); assert_eq!(split_code(message).0, "offline"); assert_eq!(split_code("a gate failed").0, "registry_refused"); } #[test] fn the_real_transport_refuses_plain_http_without_a_request() { let error = Ureq::new().get("http://git.marcospaulo.dev.br/x", 10, None).expect_err("http").to_string(); assert!(error.contains("https"), "{error}"); } #[test] fn a_304_keeps_the_cache_and_a_failed_fetch_changes_nothing() { let doc = index_json(&[entry("grammar", "x", "1.0.0", &[("a.txt", BODY)], &[])], &[]); let first = FakeTransport::new().with_response(INDEX_URL, Ok(Response { status: 200, etag: Some("\"v1\"".into()), bytes: doc.clone() })); let cache = fetch_index(&first, None, 100).unwrap(); assert_eq!((cache.fetched_at, cache.etag.as_deref()), (100, Some("\"v1\""))); let again = FakeTransport::new().with_response(INDEX_URL, Ok(Response { status: 304, etag: None, bytes: Vec::new() })); let kept = fetch_index(&again, Some(&cache), 200).unwrap(); assert_eq!((kept.fetched_at, kept.body.as_str(), kept.etag.as_deref()), (200, cache.body.as_str(), Some("\"v1\""))); let down = fetch_index(&FakeTransport::new(), Some(&cache), 300).expect_err("offline").to_string(); assert!(down.contains("offline"), "{down}"); let junk = FakeTransport::new().with(INDEX_URL, b"not json"); assert!(fetch_index(&junk, Some(&cache), 300).is_err()); } #[test] fn the_cache_round_trips_on_disk() { let dir = std::env::temp_dir().join(format!("notescraft-addons-net-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); assert_eq!(IndexCache::load(&dir).unwrap(), None); let cache = IndexCache { fetched_at: 5, etag: None, body: String::from_utf8(index_json(&[], &[])).unwrap() }; cache.save(&dir).unwrap(); assert_eq!(IndexCache::load(&dir).unwrap(), Some(cache)); std::fs::write(dir.join("index-cache.json"), b"{").unwrap(); assert!(IndexCache::load(&dir).is_err()); std::fs::remove_dir_all(&dir).unwrap(); }