test: add unit and integration tests for models, error, config, db, and edge cases

This commit is contained in:
NetworkCats
2026-03-02 23:56:51 +08:00
parent 0834a05c84
commit efedf1cd59
6 changed files with 963 additions and 0 deletions
+12
View File
@@ -64,4 +64,16 @@ mod tests {
fn rejects_empty() {
assert!(!is_cli_user_agent(""));
}
#[test]
fn matching_is_case_sensitive() {
// Real CLI tools send consistent casing; we intentionally do not
// match case-insensitively to avoid false positives.
assert!(!is_cli_user_agent("CURL/8.7.1"));
assert!(!is_cli_user_agent("Curl/8.7.1"));
assert!(!is_cli_user_agent("WGET/1.21.4"));
assert!(!is_cli_user_agent("httpie/3.2.2"));
assert!(!is_cli_user_agent("XH/0.22.0"));
assert!(!is_cli_user_agent("powershell/7.4.1"));
}
}
+160
View File
@@ -39,3 +39,163 @@ impl Config {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
// Environment variable tests must run serially because they mutate global state.
// We recover from poisoned mutexes since panics in should_panic tests are expected.
static ENV_LOCK: Mutex<()> = Mutex::new(());
fn lock_env() -> std::sync::MutexGuard<'static, ()> {
ENV_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
const CONFIG_VARS: &[&str] = &[
"LISTEN_ADDR",
"DB_PATH",
"DB_UPDATE_URL",
"DB_UPDATE_INTERVAL_HOURS",
"SITE_DOMAIN",
];
// SAFETY: These env var helpers are only called while holding ENV_LOCK,
// ensuring no concurrent mutation of the process environment.
unsafe fn clear_config_vars() {
for var in CONFIG_VARS {
unsafe { env::remove_var(var) };
}
}
unsafe fn set_var(key: &str, value: &str) {
unsafe { env::set_var(key, value) };
}
unsafe fn remove_var(key: &str) {
unsafe { env::remove_var(key) };
}
#[test]
fn defaults_when_no_env_vars() {
let _guard = lock_env();
// SAFETY: ENV_LOCK is held, serializing access to env vars.
unsafe { clear_config_vars() };
let config = Config::from_env();
assert_eq!(config.listen_addr.to_string(), "0.0.0.0:3000");
assert_eq!(config.db_path, "data/Merged-IP.mmdb");
assert!(config.db_update_url.contains("Merged-IP.mmdb"));
assert_eq!(config.db_update_interval_hours, 24);
assert_eq!(config.site_domain, "localhost");
}
#[test]
fn custom_listen_addr() {
let _guard = lock_env();
// SAFETY: ENV_LOCK is held.
unsafe {
clear_config_vars();
set_var("LISTEN_ADDR", "127.0.0.1:8080");
}
let config = Config::from_env();
assert_eq!(config.listen_addr.to_string(), "127.0.0.1:8080");
// SAFETY: ENV_LOCK is held.
unsafe { remove_var("LISTEN_ADDR") };
}
#[test]
fn custom_db_path() {
let _guard = lock_env();
// SAFETY: ENV_LOCK is held.
unsafe {
clear_config_vars();
set_var("DB_PATH", "/tmp/test.mmdb");
}
let config = Config::from_env();
assert_eq!(config.db_path, "/tmp/test.mmdb");
// SAFETY: ENV_LOCK is held.
unsafe { remove_var("DB_PATH") };
}
#[test]
fn custom_update_url() {
let _guard = lock_env();
// SAFETY: ENV_LOCK is held.
unsafe {
clear_config_vars();
set_var("DB_UPDATE_URL", "https://example.com/db.mmdb");
}
let config = Config::from_env();
assert_eq!(config.db_update_url, "https://example.com/db.mmdb");
// SAFETY: ENV_LOCK is held.
unsafe { remove_var("DB_UPDATE_URL") };
}
#[test]
fn custom_update_interval() {
let _guard = lock_env();
// SAFETY: ENV_LOCK is held.
unsafe {
clear_config_vars();
set_var("DB_UPDATE_INTERVAL_HOURS", "12");
}
let config = Config::from_env();
assert_eq!(config.db_update_interval_hours, 12);
// SAFETY: ENV_LOCK is held.
unsafe { remove_var("DB_UPDATE_INTERVAL_HOURS") };
}
#[test]
fn custom_site_domain() {
let _guard = lock_env();
// SAFETY: ENV_LOCK is held.
unsafe {
clear_config_vars();
set_var("SITE_DOMAIN", "ip.example.com");
}
let config = Config::from_env();
assert_eq!(config.site_domain, "ip.example.com");
// SAFETY: ENV_LOCK is held.
unsafe { remove_var("SITE_DOMAIN") };
}
#[test]
#[should_panic(expected = "LISTEN_ADDR must be a valid socket address")]
fn invalid_listen_addr_panics() {
let _guard = lock_env();
// SAFETY: ENV_LOCK is held.
unsafe {
clear_config_vars();
set_var("LISTEN_ADDR", "not-a-socket-addr");
}
let _config = Config::from_env();
}
#[test]
#[should_panic(expected = "DB_UPDATE_INTERVAL_HOURS must be a valid integer")]
fn invalid_update_interval_panics() {
let _guard = lock_env();
// SAFETY: ENV_LOCK is held.
unsafe {
clear_config_vars();
set_var("DB_UPDATE_INTERVAL_HOURS", "not-a-number");
}
let _config = Config::from_env();
}
}
+164
View File
@@ -91,3 +91,167 @@ pub fn lookup_city_name(db: &DbReader, ip: IpAddr) -> Option<String> {
let record: CityOnly<'_> = result.decode().ok()??;
record.city.names.english.map(str::to_owned)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
fn test_db_path() -> String {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
format!("{manifest_dir}/data/Merged-IP.mmdb")
}
fn test_reader() -> DbReader {
load_db(Path::new(&test_db_path())).expect("failed to load test DB")
}
#[test]
fn load_db_with_valid_path() {
let reader = load_db(Path::new(&test_db_path()));
assert!(reader.is_ok());
}
#[test]
fn load_db_with_invalid_path() {
let reader = load_db(Path::new("/nonexistent/path.mmdb"));
assert!(reader.is_err());
}
#[test]
fn new_shared_creates_valid_shared_db() {
let reader = test_reader();
let shared = new_shared(reader);
let loaded = shared.load();
// Verify the shared DB is functional by doing a simple lookup
let ip: IpAddr = "1.1.1.1".parse().unwrap();
let result = loaded.lookup(ip);
assert!(result.is_ok());
}
#[test]
fn lookup_returns_some_for_known_ip() {
let reader = test_reader();
let ip: IpAddr = "1.1.1.1".parse().unwrap();
let result = lookup(&reader, ip);
assert!(result.is_some());
let info = result.unwrap();
assert_eq!(info.ip, "1.1.1.1");
}
#[test]
fn lookup_returns_none_for_unroutable_ip() {
let reader = test_reader();
let ip: IpAddr = "0.0.0.0".parse().unwrap();
let result = lookup(&reader, ip);
assert!(result.is_none());
}
#[test]
fn lookup_proxy_returns_data_for_known_ip() {
let reader = test_reader();
let ip: IpAddr = "45.77.77.77".parse().unwrap();
let proxy = lookup_proxy(&reader, ip);
assert!(proxy.is_some());
let proxy = proxy.unwrap();
assert!(proxy.is_proxy);
assert!(proxy.is_hosting);
}
#[test]
fn lookup_proxy_returns_none_for_unroutable_ip() {
let reader = test_reader();
let ip: IpAddr = "0.0.0.0".parse().unwrap();
let proxy = lookup_proxy(&reader, ip);
assert!(proxy.is_none());
}
#[test]
fn lookup_asn_number_for_known_ip() {
let reader = test_reader();
let ip: IpAddr = "1.1.1.1".parse().unwrap();
let asn = lookup_asn_number(&reader, ip);
assert_eq!(asn, Some(13335));
}
#[test]
fn lookup_asn_number_returns_none_for_unroutable_ip() {
let reader = test_reader();
let ip: IpAddr = "0.0.0.0".parse().unwrap();
let asn = lookup_asn_number(&reader, ip);
assert!(asn.is_none());
}
#[test]
fn lookup_asn_org_for_known_ip() {
let reader = test_reader();
let ip: IpAddr = "1.1.1.1".parse().unwrap();
let org = lookup_asn_org(&reader, ip);
assert!(org.is_some());
assert_eq!(org.unwrap(), "Cloudflare, Inc.");
}
#[test]
fn lookup_asn_org_returns_none_for_unroutable_ip() {
let reader = test_reader();
let ip: IpAddr = "0.0.0.0".parse().unwrap();
let org = lookup_asn_org(&reader, ip);
assert!(org.is_none());
}
#[test]
fn lookup_country_name_for_known_ip() {
let reader = test_reader();
let ip: IpAddr = "45.77.77.77".parse().unwrap();
let country = lookup_country_name(&reader, ip);
assert_eq!(country.as_deref(), Some("United States"));
}
#[test]
fn lookup_country_name_returns_none_for_unroutable_ip() {
let reader = test_reader();
let ip: IpAddr = "0.0.0.0".parse().unwrap();
let country = lookup_country_name(&reader, ip);
assert!(country.is_none());
}
#[test]
fn lookup_city_name_for_known_ip() {
let reader = test_reader();
let ip: IpAddr = "45.77.77.77".parse().unwrap();
let city = lookup_city_name(&reader, ip);
assert_eq!(city.as_deref(), Some("Piscataway"));
}
#[test]
fn lookup_city_name_returns_none_for_unroutable_ip() {
let reader = test_reader();
let ip: IpAddr = "0.0.0.0".parse().unwrap();
let city = lookup_city_name(&reader, ip);
assert!(city.is_none());
}
#[test]
fn lookup_works_with_ipv6() {
let reader = test_reader();
let ip: IpAddr = "2606:4700:4700::1111".parse().unwrap();
let result = lookup(&reader, ip);
assert!(result.is_some());
let info = result.unwrap();
assert_eq!(info.ip, "2606:4700:4700::1111");
assert!(info.asn.is_some());
assert_eq!(
info.asn.as_ref().unwrap().autonomous_system_number,
Some(13335)
);
}
#[test]
fn partial_lookups_work_with_ipv6() {
let reader = test_reader();
let ip: IpAddr = "2606:4700:4700::1111".parse().unwrap();
assert_eq!(lookup_asn_number(&reader, ip), Some(13335));
assert!(lookup_asn_org(&reader, ip).is_some());
assert!(lookup_proxy(&reader, ip).is_some());
}
}
+54
View File
@@ -19,3 +19,57 @@ impl IntoResponse for AppError {
(status, message).into_response()
}
}
#[cfg(test)]
mod tests {
use super::*;
use http_body_util::BodyExt;
async fn check_error_response(error: AppError, expected_status: StatusCode, expected_body: &str) {
let response = error.into_response();
assert_eq!(response.status(), expected_status);
let body = response.into_body().collect().await.unwrap().to_bytes();
let text = String::from_utf8(body.to_vec()).unwrap();
assert_eq!(text, expected_body);
}
#[tokio::test]
async fn ip_not_found_returns_404() {
check_error_response(
AppError::IpNotFound,
StatusCode::NOT_FOUND,
"IP not found in database",
)
.await;
}
#[tokio::test]
async fn invalid_ip_returns_400() {
check_error_response(
AppError::InvalidIp,
StatusCode::BAD_REQUEST,
"Invalid IP address",
)
.await;
}
#[tokio::test]
async fn missing_client_ip_returns_400() {
check_error_response(
AppError::MissingClientIp,
StatusCode::BAD_REQUEST,
"Missing client IP address",
)
.await;
}
#[tokio::test]
async fn db_lookup_failed_returns_500() {
check_error_response(
AppError::DbLookupFailed,
StatusCode::INTERNAL_SERVER_ERROR,
"Database lookup failed",
)
.await;
}
}
+424
View File
@@ -423,3 +423,427 @@ pub fn get_en_name(names: &Option<Names>) -> &str {
.and_then(|n| n.english.as_deref())
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
fn empty_mmdb_names<'a>() -> MmdbNames<'a> {
MmdbNames::default()
}
fn english_only_names<'a>() -> MmdbNames<'a> {
MmdbNames {
english: Some("TestName"),
..Default::default()
}
}
fn full_names<'a>() -> MmdbNames<'a> {
MmdbNames {
german: Some("Deutsch"),
english: Some("English"),
spanish: Some("Espanol"),
french: Some("Francais"),
japanese: Some("Nihongo"),
brazilian_portuguese: Some("Portugues"),
russian: Some("Russkiy"),
simplified_chinese: Some("Zhongwen"),
}
}
// --- MmdbNames::is_empty tests ---
#[test]
fn mmdb_names_is_empty_when_all_none() {
let names = empty_mmdb_names();
assert!(names.is_empty());
}
#[test]
fn mmdb_names_is_not_empty_with_english() {
let names = english_only_names();
assert!(!names.is_empty());
}
#[test]
fn mmdb_names_is_not_empty_with_non_english_only() {
let names = MmdbNames {
german: Some("Berlin"),
..Default::default()
};
assert!(!names.is_empty());
}
#[test]
fn mmdb_names_is_not_empty_when_fully_populated() {
let names = full_names();
assert!(!names.is_empty());
}
// --- convert_names tests ---
#[test]
fn convert_names_returns_none_for_empty() {
let names = empty_mmdb_names();
assert!(convert_names(&names).is_none());
}
#[test]
fn convert_names_returns_some_with_english() {
let names = english_only_names();
let result = convert_names(&names).expect("should produce Some");
assert_eq!(result.english.as_deref(), Some("TestName"));
assert!(result.german.is_none());
assert!(result.spanish.is_none());
}
#[test]
fn convert_names_copies_all_languages() {
let names = full_names();
let result = convert_names(&names).expect("should produce Some");
assert_eq!(result.german.as_deref(), Some("Deutsch"));
assert_eq!(result.english.as_deref(), Some("English"));
assert_eq!(result.spanish.as_deref(), Some("Espanol"));
assert_eq!(result.french.as_deref(), Some("Francais"));
assert_eq!(result.japanese.as_deref(), Some("Nihongo"));
assert_eq!(result.brazilian_portuguese.as_deref(), Some("Portugues"));
assert_eq!(result.russian.as_deref(), Some("Russkiy"));
assert_eq!(result.simplified_chinese.as_deref(), Some("Zhongwen"));
}
// --- has_* predicate tests ---
#[test]
fn has_city_false_for_default() {
let city = MmdbCity::default();
assert!(!has_city(&city));
}
#[test]
fn has_city_true_with_geoname_id() {
let city = MmdbCity {
geoname_id: Some(12345),
names: empty_mmdb_names(),
};
assert!(has_city(&city));
}
#[test]
fn has_city_true_with_names() {
let city = MmdbCity {
geoname_id: None,
names: english_only_names(),
};
assert!(has_city(&city));
}
#[test]
fn has_continent_false_for_default() {
let continent = MmdbContinent::default();
assert!(!has_continent(&continent));
}
#[test]
fn has_continent_true_with_code() {
let continent = MmdbContinent {
code: Some("NA"),
geoname_id: None,
names: empty_mmdb_names(),
};
assert!(has_continent(&continent));
}
#[test]
fn has_country_false_for_default() {
let country = MmdbCountry::default();
assert!(!has_country(&country));
}
#[test]
fn has_country_true_with_iso_code() {
let country = MmdbCountry {
geoname_id: None,
iso_code: Some("US"),
names: empty_mmdb_names(),
};
assert!(has_country(&country));
}
#[test]
fn has_location_false_for_default() {
let loc = MmdbLocation::default();
assert!(!has_location(&loc));
}
#[test]
fn has_location_true_with_latitude() {
let loc = MmdbLocation {
latitude: Some(40.0),
..Default::default()
};
assert!(has_location(&loc));
}
#[test]
fn has_location_true_with_timezone() {
let loc = MmdbLocation {
time_zone: Some("America/New_York"),
..Default::default()
};
assert!(has_location(&loc));
}
#[test]
fn has_postal_false_for_default() {
let postal = MmdbPostal::default();
assert!(!has_postal(&postal));
}
#[test]
fn has_postal_true_with_code() {
let postal = MmdbPostal {
code: Some("10001"),
};
assert!(has_postal(&postal));
}
#[test]
fn has_asn_false_for_default() {
let asn = MmdbAsn::default();
assert!(!has_asn(&asn));
}
#[test]
fn has_asn_true_with_number() {
let asn = MmdbAsn {
autonomous_system_number: Some(13335),
autonomous_system_organization: None,
as_domain: None,
};
assert!(has_asn(&asn));
}
#[test]
fn has_asn_true_with_org() {
let asn = MmdbAsn {
autonomous_system_number: None,
autonomous_system_organization: Some("Cloudflare"),
as_domain: None,
};
assert!(has_asn(&asn));
}
#[test]
fn has_asn_true_with_domain() {
let asn = MmdbAsn {
autonomous_system_number: None,
autonomous_system_organization: None,
as_domain: Some("cloudflare.com"),
};
assert!(has_asn(&asn));
}
// --- from_mmdb_record tests ---
#[test]
fn from_mmdb_record_with_empty_record() {
let record = MmdbRecord::default();
let ip = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4));
let info = from_mmdb_record(ip, &record);
assert_eq!(info.ip, "1.2.3.4");
assert!(info.city.is_none());
assert!(info.continent.is_none());
assert!(info.country.is_none());
assert!(info.location.is_none());
assert!(info.postal.is_none());
assert!(info.registered_country.is_none());
assert!(info.subdivisions.is_none());
assert!(info.asn.is_none());
assert!(!info.proxy.is_proxy);
assert!(!info.proxy.is_vpn);
assert!(!info.proxy.is_tor);
assert!(!info.proxy.is_hosting);
}
#[test]
fn from_mmdb_record_with_populated_record() {
let record = MmdbRecord {
city: MmdbCity {
geoname_id: Some(5101798),
names: english_only_names(),
},
continent: MmdbContinent {
code: Some("NA"),
geoname_id: Some(6255149),
names: english_only_names(),
},
country: MmdbCountry {
geoname_id: Some(6252001),
iso_code: Some("US"),
names: english_only_names(),
},
location: MmdbLocation {
accuracy_radius: Some(1000),
latitude: Some(40.5),
longitude: Some(-74.3),
metro_code: Some(501),
time_zone: Some("America/New_York"),
},
postal: MmdbPostal {
code: Some("08854"),
},
registered_country: MmdbCountry {
geoname_id: Some(6252001),
iso_code: Some("US"),
names: english_only_names(),
},
subdivisions: vec![MmdbSubdivision {
geoname_id: Some(5101760),
iso_code: Some("NJ"),
names: english_only_names(),
}],
asn: MmdbAsn {
autonomous_system_number: Some(20473),
autonomous_system_organization: Some("The Constant Company"),
as_domain: Some("vultr.com"),
},
proxy: MmdbProxy {
is_proxy: true,
is_hosting: true,
..Default::default()
},
};
let ip = IpAddr::V4(Ipv4Addr::new(45, 77, 77, 77));
let info = from_mmdb_record(ip, &record);
assert_eq!(info.ip, "45.77.77.77");
let city = info.city.as_ref().expect("city should be present");
assert_eq!(city.geoname_id, Some(5101798));
let continent = info
.continent
.as_ref()
.expect("continent should be present");
assert_eq!(continent.code.as_deref(), Some("NA"));
let country = info.country.as_ref().expect("country should be present");
assert_eq!(country.iso_code.as_deref(), Some("US"));
let location = info.location.as_ref().expect("location should be present");
assert_eq!(location.accuracy_radius, Some(1000));
assert_eq!(location.latitude, Some(40.5));
assert_eq!(location.time_zone.as_deref(), Some("America/New_York"));
let postal = info.postal.as_ref().expect("postal should be present");
assert_eq!(postal.code.as_deref(), Some("08854"));
assert!(info.registered_country.is_some());
let subdivisions = info
.subdivisions
.as_ref()
.expect("subdivisions should be present");
assert_eq!(subdivisions.len(), 1);
assert_eq!(subdivisions[0].iso_code.as_deref(), Some("NJ"));
let asn = info.asn.as_ref().expect("asn should be present");
assert_eq!(asn.autonomous_system_number, Some(20473));
assert_eq!(
asn.autonomous_system_organization.as_deref(),
Some("The Constant Company")
);
assert_eq!(asn.as_domain.as_deref(), Some("vultr.com"));
assert!(info.proxy.is_proxy);
assert!(info.proxy.is_hosting);
assert!(!info.proxy.is_vpn);
assert!(!info.proxy.is_tor);
}
#[test]
fn from_mmdb_record_with_ipv6() {
let record = MmdbRecord::default();
let ip = IpAddr::V6(Ipv6Addr::new(0x2606, 0x4700, 0x4700, 0, 0, 0, 0, 0x1111));
let info = from_mmdb_record(ip, &record);
assert_eq!(info.ip, "2606:4700:4700::1111");
}
#[test]
fn from_mmdb_record_empty_subdivisions_become_none() {
let record = MmdbRecord {
subdivisions: vec![],
..Default::default()
};
let ip = IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1));
let info = from_mmdb_record(ip, &record);
assert!(info.subdivisions.is_none());
}
#[test]
fn from_mmdb_record_proxy_flags_are_forwarded() {
let record = MmdbRecord {
proxy: MmdbProxy {
is_proxy: true,
is_vpn: true,
is_tor: true,
is_hosting: true,
is_cdn: true,
is_school: true,
is_anonymous: true,
},
..Default::default()
};
let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
let info = from_mmdb_record(ip, &record);
assert!(info.proxy.is_proxy);
assert!(info.proxy.is_vpn);
assert!(info.proxy.is_tor);
assert!(info.proxy.is_hosting);
assert!(info.proxy.is_cdn);
assert!(info.proxy.is_school);
assert!(info.proxy.is_anonymous);
}
// --- get_en_name tests ---
#[test]
fn get_en_name_returns_empty_for_none() {
let names: Option<Names> = None;
assert_eq!(get_en_name(&names), "");
}
#[test]
fn get_en_name_returns_empty_when_english_missing() {
let names = Some(Names {
german: Some("Berlin".to_string()),
english: None,
spanish: None,
french: None,
japanese: None,
brazilian_portuguese: None,
russian: None,
simplified_chinese: None,
});
assert_eq!(get_en_name(&names), "");
}
#[test]
fn get_en_name_returns_english_value() {
let names = Some(Names {
english: Some("New York".to_string()),
german: None,
spanish: None,
french: None,
japanese: None,
brazilian_portuguese: None,
russian: None,
simplified_chinese: None,
});
assert_eq!(get_en_name(&names), "New York");
}
}
+149
View File
@@ -280,3 +280,152 @@ async fn ipv6_via_cf_header() {
assert_eq!(status, StatusCode::OK);
assert_eq!(body.trim(), "2606:4700:4700::1111");
}
// --- Edge case tests ---
#[tokio::test]
async fn empty_ip_query_falls_back_to_cf_header() {
let app = build_test_app();
let (status, body) = get_with_headers(
&app,
"/json?ip=",
vec![("CF-Connecting-IP", "8.8.8.8")],
)
.await;
assert_eq!(status, StatusCode::OK);
let json: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(json["ip"], "8.8.8.8");
}
#[tokio::test]
async fn whitespace_ip_query_falls_back_to_cf_header() {
let app = build_test_app();
let (status, body) = get_with_headers(
&app,
"/json?ip=%20",
vec![("CF-Connecting-IP", "8.8.8.8")],
)
.await;
assert_eq!(status, StatusCode::OK);
let json: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(json["ip"], "8.8.8.8");
}
#[tokio::test]
async fn ip_not_in_database_returns_500() {
let app = build_test_app();
// 0.0.0.0 is unlikely to be in a geo database
let (status, _) = get(&app, "/json?ip=0.0.0.0").await;
assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
}
#[tokio::test]
async fn cli_with_query_param_returns_html() {
let app = build_test_app();
let (status, body) = get_with_headers(
&app,
"/?ip=45.77.77.77",
vec![
("User-Agent", "curl/8.7.1"),
("CF-Connecting-IP", "1.2.3.4"),
],
)
.await;
assert_eq!(status, StatusCode::OK);
// When CLI sends ?ip=, the shortcut is bypassed and HTML is returned
assert!(body.contains("<!DOCTYPE html>"));
assert!(body.contains("45.77.77.77"));
}
#[tokio::test]
async fn accept_json_with_query_param_returns_html() {
let app = build_test_app();
let (status, body) = get_with_headers(
&app,
"/?ip=45.77.77.77",
vec![
("Accept", "application/json"),
("CF-Connecting-IP", "8.8.8.8"),
],
)
.await;
assert_eq!(status, StatusCode::OK);
// When Accept: application/json is sent with ?ip=, the JSON shortcut is bypassed
assert!(body.contains("<!DOCTYPE html>"));
assert!(body.contains("45.77.77.77"));
}
#[tokio::test]
async fn ip_endpoint_via_cf_header() {
let app = build_test_app();
let (status, body) = get_with_headers(
&app,
"/ip",
vec![("CF-Connecting-IP", "1.1.1.1")],
)
.await;
assert_eq!(status, StatusCode::OK);
assert_eq!(body.trim(), "1.1.1.1");
}
#[tokio::test]
async fn asn_endpoint_via_cf_header() {
let app = build_test_app();
let (status, body) = get_with_headers(
&app,
"/asn",
vec![("CF-Connecting-IP", "1.1.1.1")],
)
.await;
assert_eq!(status, StatusCode::OK);
assert_eq!(body.trim(), "AS13335");
}
#[tokio::test]
async fn org_endpoint_via_cf_header() {
let app = build_test_app();
let (status, body) = get_with_headers(
&app,
"/org",
vec![("CF-Connecting-IP", "1.1.1.1")],
)
.await;
assert_eq!(status, StatusCode::OK);
assert_eq!(body.trim(), "Cloudflare, Inc.");
}
#[tokio::test]
async fn country_endpoint_via_cf_header() {
let app = build_test_app();
let (status, body) = get_with_headers(
&app,
"/country",
vec![("CF-Connecting-IP", "45.77.77.77")],
)
.await;
assert_eq!(status, StatusCode::OK);
assert_eq!(body.trim(), "United States");
}
#[tokio::test]
async fn invalid_ip_on_field_endpoints_returns_400() {
let app = build_test_app();
let endpoints = ["/ip", "/asn", "/org", "/country", "/city", "/proxy", "/vpn", "/hosting", "/tor"];
for endpoint in endpoints {
let uri = format!("{endpoint}?ip=invalid");
let (status, _) = get(&app, &uri).await;
assert_eq!(status, StatusCode::BAD_REQUEST, "expected 400 for {endpoint}?ip=invalid");
}
}
#[tokio::test]
async fn field_endpoints_without_ip_or_header_return_400() {
let app = build_test_app();
let endpoints = ["/ip", "/asn", "/org", "/country", "/city", "/proxy", "/vpn", "/hosting", "/tor", "/json"];
for endpoint in endpoints {
let (status, _) = get(&app, endpoint).await;
assert_eq!(status, StatusCode::BAD_REQUEST, "expected 400 for {endpoint} without IP");
}
}