use super::*;
use crate::ServeError;
fn addrs(list: &[&str]) -> Vec<SocketAddr> {
list.iter().map(|s| s.parse().expect("addr")).collect()
}
#[test]
fn loopback_is_dialable_without_any_flag() {
for a in ["127.0.0.1:11434", "127.0.0.2:80", "[::1]:11434"] {
assert_eq!(screen(addrs(&[a]), false), addrs(&[a]), "{a} needs no flag");
}
}
#[test]
fn a_private_address_needs_the_flag() {
let a = addrs(&["192.168.1.5:11434"]);
assert_eq!(screen(a.clone(), false), addrs(&[]), "refused bare");
assert_eq!(screen(a.clone(), true), a, "admitted with the flag");
}
#[test]
fn a_public_address_is_never_dialable() {
for a in ["8.8.8.8:80", "[2606:4700::1111]:443"] {
assert_eq!(screen(addrs(&[a]), false), addrs(&[]), "{a}");
assert_eq!(
screen(addrs(&[a]), true),
addrs(&[]),
"{a} even with the flag"
);
}
}
#[test]
fn the_metadata_endpoint_is_never_dialable_however_it_is_spelled() {
for a in [
"169.254.169.254:80",
"[::ffff:169.254.169.254]:80",
"[fe80::1]:80",
] {
assert_eq!(screen(addrs(&[a]), false), addrs(&[]), "{a}");
assert_eq!(
screen(addrs(&[a]), true),
addrs(&[]),
"{a} even with the flag"
);
}
}
#[test]
fn every_candidate_is_screened_and_not_merely_the_first() {
let mixed = addrs(&["8.8.8.8:11434", "127.0.0.1:11434"]);
assert_eq!(
screen(mixed, false),
addrs(&["127.0.0.1:11434"]),
"the public candidate is skipped, the loopback one taken"
);
let mixed = addrs(&["127.0.0.1:11434", "8.8.8.8:11434"]);
assert_eq!(screen(mixed, false), addrs(&["127.0.0.1:11434"]));
let all_bad = addrs(&["8.8.8.8:80", "169.254.169.254:80", "0.0.0.0:80"]);
assert_eq!(screen(all_bad, true), addrs(&[]));
}
#[test]
fn a_name_that_resolves_to_nothing_is_not_dialable() {
assert_eq!(screen(addrs(&[]), true), addrs(&[]));
}
#[tokio::test]
async fn a_loopback_backend_is_accepted_and_names_its_own_authority() {
let backend = TcpBackend::new("http://127.0.0.1:11434", false)
.await
.expect("loopback must be accepted");
assert_eq!(
backend.authority(),
"127.0.0.1:11434",
"the Host header names the backend, port included"
);
}
#[tokio::test]
async fn a_named_loopback_backend_is_accepted() {
let backend = TcpBackend::new("http://localhost:11434", false)
.await
.expect("localhost must be accepted");
assert_eq!(backend.authority(), "localhost:11434");
}
#[tokio::test]
async fn the_default_port_is_supplied_when_the_url_omits_it() {
let backend = TcpBackend::new("http://127.0.0.1", false)
.await
.expect("ok");
assert_eq!(backend.authority(), "127.0.0.1:80");
}
#[tokio::test]
async fn a_public_backend_is_refused_before_the_listener_starts() {
let err = TcpBackend::new("http://8.8.8.8:80", true)
.await
.expect_err("a public backend must be refused");
match &err {
ServeError::BackendNotLocal { url } => assert_eq!(url, "http://8.8.8.8:80"),
other => panic!("expected BackendNotLocal, got {other:?}"),
}
assert!(!err.is_retryable(), "and it is the operator's to fix");
}
#[tokio::test]
async fn a_non_http_backend_is_refused() {
for url in [
"https://127.0.0.1:11434",
"file:///etc/passwd",
"ftp://127.0.0.1",
"not a url",
"",
] {
assert!(
TcpBackend::new(url, true).await.is_err(),
"{url:?} must be refused"
);
}
}
#[tokio::test]
async fn a_screened_address_is_the_address_actually_dialled() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind");
let bound = listener.local_addr().expect("addr");
let backend = TcpBackend::new(&format!("http://127.0.0.1:{}", bound.port()), false)
.await
.expect("loopback");
let accepted = tokio::spawn(async move { listener.accept().await.map(|(_, peer)| peer) });
let stream = backend.connect().await.expect("connect");
assert_eq!(stream.peer_addr().expect("peer"), bound);
let peer = accepted.await.expect("task").expect("accept");
assert!(peer.ip().is_loopback(), "and the caller came from loopback");
}
#[test]
fn every_admissible_address_is_offered_in_order() {
let mixed = addrs(&["[::1]:11434", "8.8.8.8:11434", "127.0.0.1:11434"]);
assert_eq!(
screen(mixed, false),
addrs(&["[::1]:11434", "127.0.0.1:11434"]),
"both loopback candidates, in the order the resolver gave them, \
and the public one dropped from between them"
);
}
#[tokio::test]
async fn an_ipv6_literal_backend_is_accepted_and_keeps_its_brackets() {
let backend = TcpBackend::new("http://[::1]:11434", false)
.await
.expect("::1 is loopback");
assert_eq!(
backend.authority(),
"[::1]:11434",
"the Host header needs the brackets back — `::1:11434` cannot be \
read as an address and a port"
);
}
#[tokio::test]
async fn an_ipv4_literal_backend_gains_no_brackets() {
let backend = TcpBackend::new("http://127.0.0.1:11434", false)
.await
.expect("loopback");
assert_eq!(backend.authority(), "127.0.0.1:11434");
}
#[tokio::test]
async fn a_url_this_crate_cannot_use_is_not_reported_as_a_locality_verdict() {
for url in [
"not a url at all",
"https://127.0.0.1:11434",
"http://",
"ftp://127.0.0.1:11434",
"http://127.0.0.1:11434/v1",
"http://127.0.0.1:11434/v1/",
"http://127.0.0.1:11434?x=1",
"http://127.0.0.1:11434#frag",
] {
match TcpBackend::new(url, false).await {
Err(ServeError::InvalidBackendUrl { url: named }) => {
assert_eq!(named, url, "the error must name what was refused");
}
other => panic!("{url} should be InvalidBackendUrl, got {other:?}"),
}
}
}
#[tokio::test]
async fn an_address_this_listener_may_not_dial_is_still_a_locality_verdict() {
match TcpBackend::new("http://8.8.8.8:11434", false).await {
Err(e @ ServeError::BackendNotLocal { .. }) => {
assert!(!e.is_retryable(), "the operator's to fix: {e}");
}
other => panic!("expected BackendNotLocal, got {other:?}"),
}
}
#[tokio::test]
async fn a_host_that_resolves_to_nothing_is_retryable() {
match TcpBackend::new("http://nothing.invalid:11434", false).await {
Err(e @ ServeError::BackendUnresolvable { .. }) => {
assert!(e.is_retryable(), "a resolver outage clears: {e}");
}
other => panic!("expected BackendUnresolvable, got {other:?}"),
}
}
#[tokio::test]
async fn a_backend_url_with_no_path_is_accepted_either_way_it_is_written() {
for url in ["http://127.0.0.1:11434", "http://127.0.0.1:11434/"] {
let backend = TcpBackend::new(url, false)
.await
.unwrap_or_else(|e| panic!("{url} must be usable: {e}"));
assert_eq!(backend.authority(), "127.0.0.1:11434");
}
}
#[tokio::test]
async fn a_path_is_refused_before_the_host_is_resolved() {
let url = "http://nothing.invalid:11434/v1";
match TcpBackend::new(url, false).await {
Err(ServeError::InvalidBackendUrl { url: named }) => assert_eq!(named, url),
other => panic!("the path must be refused before resolving, got {other:?}"),
}
}
#[tokio::test]
async fn a_backend_url_carrying_userinfo_is_refused() {
for url in [
"http://user:pass@127.0.0.1:11434",
"http://user@127.0.0.1:11434",
"http://:pass@127.0.0.1:11434",
] {
match TcpBackend::new(url, false).await {
Err(ServeError::InvalidBackendUrl { url: named }) => assert_eq!(named, url),
other => panic!("{url} should be InvalidBackendUrl, got {other:?}"),
}
}
}