mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
chore(repo): remove dead code and useless tests (#3375)
This commit is contained in:
1 parent
7e177a1c90
commit
1cd62eb661
1391 files changed
+783
-120415
No files matched your search
+1
-211
@@ -1,127 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::{env, fs, path::Path};
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum GeoipSourceConfig {
|
||||
Filesystem {
|
||||
maxmind_db_path: Option<String>,
|
||||
},
|
||||
S3 {
|
||||
maxmind_db_path: String,
|
||||
s3_bucket: String,
|
||||
s3_key: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl GeoipSourceConfig {
|
||||
pub fn maxmind_db_path(&self) -> Option<String> {
|
||||
match self {
|
||||
Self::Filesystem { maxmind_db_path } => maxmind_db_path.clone(),
|
||||
Self::S3 {
|
||||
maxmind_db_path, ..
|
||||
} => Some(maxmind_db_path.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct GeoipS3Config {
|
||||
pub endpoint: String,
|
||||
pub region: String,
|
||||
pub access_key_id: String,
|
||||
pub secret_access_key: String,
|
||||
}
|
||||
|
||||
pub fn read_geoip_s3_config_from_env(source: &GeoipSourceConfig) -> Option<GeoipS3Config> {
|
||||
read_geoip_s3_config(source, env_value)
|
||||
}
|
||||
|
||||
fn read_geoip_s3_config<F>(source: &GeoipSourceConfig, mut read_var: F) -> Option<GeoipS3Config>
|
||||
where
|
||||
F: FnMut(&str) -> Option<String>,
|
||||
{
|
||||
match source {
|
||||
GeoipSourceConfig::S3 { .. } => Some(GeoipS3Config {
|
||||
endpoint: read_var("FLUXER_S3_ENDPOINT").unwrap_or_default(),
|
||||
region: read_var("FLUXER_S3_REGION").unwrap_or_default(),
|
||||
access_key_id: read_var("FLUXER_S3_ACCESS_KEY_ID").unwrap_or_default(),
|
||||
secret_access_key: read_var("FLUXER_S3_SECRET_ACCESS_KEY").unwrap_or_default(),
|
||||
}),
|
||||
GeoipSourceConfig::Filesystem { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_geoip_source_config(raw_value: &str, service_name: &str) -> GeoipSourceConfig {
|
||||
let trimmed = raw_value.trim();
|
||||
if trimmed.is_empty() {
|
||||
return GeoipSourceConfig::Filesystem {
|
||||
maxmind_db_path: None,
|
||||
};
|
||||
}
|
||||
if !trimmed.starts_with("s3://") {
|
||||
return GeoipSourceConfig::Filesystem {
|
||||
maxmind_db_path: Some(trimmed.to_owned()),
|
||||
};
|
||||
}
|
||||
parse_geoip_s3_source_config(trimmed, service_name)
|
||||
}
|
||||
|
||||
fn parse_geoip_s3_source_config(raw_value: &str, service_name: &str) -> GeoipSourceConfig {
|
||||
let url = reqwest::Url::parse(raw_value)
|
||||
.unwrap_or_else(|err| panic!("invalid GeoIP S3 URL {}: {}", raw_value, err));
|
||||
let s3_bucket = url.host_str().unwrap_or("").to_owned();
|
||||
if s3_bucket.is_empty() {
|
||||
panic!("invalid GeoIP S3 URL (missing bucket): {raw_value}");
|
||||
}
|
||||
let s3_key = percent_decode(url.path().trim_start_matches('/'));
|
||||
if s3_key.is_empty() {
|
||||
panic!("invalid GeoIP S3 URL (missing object key): {raw_value}");
|
||||
}
|
||||
let maxmind_db_path =
|
||||
geoip_runtime_path(&resolve_geoip_download_path(&url, raw_value), service_name);
|
||||
GeoipSourceConfig::S3 {
|
||||
maxmind_db_path,
|
||||
s3_bucket,
|
||||
s3_key,
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_geoip_download_path(url: &reqwest::Url, raw_value: &str) -> String {
|
||||
let Some(download_path) = url
|
||||
.query_pairs()
|
||||
.find(|(key, _)| key == "download_path")
|
||||
.map(|(_, value)| value.into_owned())
|
||||
else {
|
||||
panic!("invalid GeoIP S3 URL (missing query parameter \"download_path\"): {raw_value}");
|
||||
};
|
||||
require_absolute_path(&download_path, "download_path", raw_value)
|
||||
}
|
||||
|
||||
fn require_absolute_path(value: &str, param: &str, raw_value: &str) -> String {
|
||||
if !Path::new(value).is_absolute() {
|
||||
panic!("GeoIP S3 URL query parameter \"{param}\" must be an absolute path: {raw_value}");
|
||||
}
|
||||
value.to_owned()
|
||||
}
|
||||
|
||||
fn geoip_runtime_path(configured_path: &str, service_name: &str) -> String {
|
||||
let basename = Path::new(configured_path)
|
||||
.file_name()
|
||||
.and_then(|value| value.to_str())
|
||||
.unwrap_or("GeoLite2-City.mmdb");
|
||||
Path::new("/tmp/fluxer/geoip")
|
||||
.join(service_name)
|
||||
.join(basename)
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
fn percent_decode(value: &str) -> String {
|
||||
urlencoding::decode(value)
|
||||
.map(|value| value.into_owned())
|
||||
.unwrap_or_else(|_| value.to_owned())
|
||||
}
|
||||
use std::{env, fs};
|
||||
|
||||
pub fn env_value(name: &str) -> Option<String> {
|
||||
resolve_env_value(name, |key| env::var(key).ok()).unwrap_or_else(|error| panic!("{error}"))
|
||||
@@ -355,86 +234,6 @@ pub fn normalize_public_endpoint_from_env(url: &str) -> String {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parses_filesystem_geoip_source() {
|
||||
let source = parse_geoip_source_config("/data/GeoLite2-City.mmdb", "test");
|
||||
assert_eq!(
|
||||
source,
|
||||
GeoipSourceConfig::Filesystem {
|
||||
maxmind_db_path: Some("/data/GeoLite2-City.mmdb".to_owned()),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_empty_geoip_source() {
|
||||
let source = parse_geoip_source_config("", "test");
|
||||
assert_eq!(
|
||||
source,
|
||||
GeoipSourceConfig::Filesystem {
|
||||
maxmind_db_path: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_s3_geoip_source() {
|
||||
let source = parse_geoip_source_config(
|
||||
"s3://geoip/GeoLite2-City.mmdb?download_path=/tmp/city.mmdb",
|
||||
"test_svc",
|
||||
);
|
||||
assert_eq!(
|
||||
source,
|
||||
GeoipSourceConfig::S3 {
|
||||
maxmind_db_path: "/tmp/fluxer/geoip/test_svc/city.mmdb".to_owned(),
|
||||
s3_bucket: "geoip".to_owned(),
|
||||
s3_key: "GeoLite2-City.mmdb".to_owned(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reads_geoip_s3_config_only_for_s3_source() {
|
||||
let source = parse_geoip_source_config(
|
||||
"s3://geoip/GeoLite2-City.mmdb?download_path=/tmp/city.mmdb",
|
||||
"test_svc",
|
||||
);
|
||||
let config = read_geoip_s3_config(&source, |name| {
|
||||
Some(
|
||||
match name {
|
||||
"FLUXER_S3_ENDPOINT" => "https://s3.example.test",
|
||||
"FLUXER_S3_REGION" => "ewr1",
|
||||
"FLUXER_S3_ACCESS_KEY_ID" => "access",
|
||||
"FLUXER_S3_SECRET_ACCESS_KEY" => "secret",
|
||||
_ => "",
|
||||
}
|
||||
.to_owned(),
|
||||
)
|
||||
})
|
||||
.expect("s3 source should read s3 config");
|
||||
|
||||
assert_eq!(
|
||||
config,
|
||||
GeoipS3Config {
|
||||
endpoint: "https://s3.example.test".to_owned(),
|
||||
region: "ewr1".to_owned(),
|
||||
access_key_id: "access".to_owned(),
|
||||
secret_access_key: "secret".to_owned(),
|
||||
}
|
||||
);
|
||||
|
||||
let filesystem_source = parse_geoip_source_config("/tmp/city.mmdb", "test_svc");
|
||||
assert!(read_geoip_s3_config(&filesystem_source, |_| None).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_base_path_strips_slashes() {
|
||||
assert_eq!(normalize_base_path("/foo/bar/"), "/foo/bar");
|
||||
assert_eq!(normalize_base_path("foo"), "/foo");
|
||||
assert_eq!(normalize_base_path(""), "");
|
||||
assert_eq!(normalize_base_path("/"), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_https_install_is_untouched() {
|
||||
for url in [
|
||||
@@ -953,13 +752,4 @@ mod tests {
|
||||
assert!(read_bool_env("FLUXER_COMMON_TEST_BLANK_SPACES", true));
|
||||
assert!(read_bool_env("FLUXER_COMMON_TEST_BLANK_TRUE", false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trim_trailing_slash_works() {
|
||||
assert_eq!(
|
||||
trim_trailing_slash("https://example.com/"),
|
||||
"https://example.com"
|
||||
);
|
||||
assert_eq!(trim_trailing_slash(""), "");
|
||||
}
|
||||
}
|
||||
@@ -1,372 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use crate::config::{GeoipS3Config, GeoipSourceConfig};
|
||||
use aws_credential_types::Credentials;
|
||||
use aws_sigv4::{
|
||||
http_request::{
|
||||
PayloadChecksumKind, PercentEncodingMode, SignableBody, SignableRequest, SigningSettings,
|
||||
UriPathNormalizationMode, sign,
|
||||
},
|
||||
sign::v4,
|
||||
};
|
||||
use axum::http::HeaderMap;
|
||||
use maxminddb::{Reader, geoip2};
|
||||
use moka::sync::Cache;
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io,
|
||||
net::IpAddr,
|
||||
path::Path,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
const DEFAULT_COUNTRY_CODE: &str = "US";
|
||||
const GEOIP_CACHE_TTL: Duration = Duration::from_secs(10 * 60);
|
||||
const GEOIP_CACHE_MAX_ENTRIES: u64 = 4096;
|
||||
|
||||
pub struct GeoipConfig {
|
||||
pub geoip_source: GeoipSourceConfig,
|
||||
pub geoip_s3_config: Option<GeoipS3Config>,
|
||||
pub trust_client_ip_header: bool,
|
||||
pub client_ip_header_name: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct GeoipLookup {
|
||||
pub country_code: Option<String>,
|
||||
pub region_code: Option<String>,
|
||||
pub latitude: Option<f64>,
|
||||
pub longitude: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct GeoipResolver {
|
||||
reader: Option<Reader<Vec<u8>>>,
|
||||
cache: Cache<IpAddr, GeoipLookup>,
|
||||
trust_client_ip_header: bool,
|
||||
client_ip_header_name: String,
|
||||
}
|
||||
|
||||
impl GeoipResolver {
|
||||
pub fn from_config(config: &GeoipConfig) -> Self {
|
||||
let db_path = prepare_geoip_database(&config.geoip_source, config.geoip_s3_config.as_ref())
|
||||
.unwrap_or_else(|err| panic!("failed to prepare GeoIP database: {err:#}"));
|
||||
let reader = if let Some(path) = db_path.as_deref() {
|
||||
open_reader(path)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Self {
|
||||
reader,
|
||||
cache: Cache::builder()
|
||||
.max_capacity(GEOIP_CACHE_MAX_ENTRIES)
|
||||
.time_to_live(GEOIP_CACHE_TTL)
|
||||
.build(),
|
||||
trust_client_ip_header: config.trust_client_ip_header,
|
||||
client_ip_header_name: config.client_ip_header_name.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lookup(&self, headers: &HeaderMap) -> GeoipLookup {
|
||||
let Some(reader) = self.reader.as_ref() else {
|
||||
return GeoipLookup::default();
|
||||
};
|
||||
let Some(ip) = extract_client_ip(
|
||||
headers,
|
||||
self.trust_client_ip_header,
|
||||
&self.client_ip_header_name,
|
||||
) else {
|
||||
return GeoipLookup::default();
|
||||
};
|
||||
if let Some(cached) = self.cache.get(&ip) {
|
||||
return cached;
|
||||
}
|
||||
let result = lookup_geoip(reader, ip);
|
||||
self.cache.insert(ip, result.clone());
|
||||
result
|
||||
}
|
||||
|
||||
pub fn country_code(&self, headers: &HeaderMap) -> String {
|
||||
self.lookup(headers)
|
||||
.country_code
|
||||
.unwrap_or_else(|| DEFAULT_COUNTRY_CODE.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
fn open_reader(path: &str) -> Option<Reader<Vec<u8>>> {
|
||||
match Reader::open_readfile(path) {
|
||||
Ok(reader) => Some(reader),
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
geoip_db_path = path,
|
||||
?error,
|
||||
"failed to open GeoIP database; geoip will be empty"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn lookup_geoip(reader: &Reader<Vec<u8>>, ip: IpAddr) -> GeoipLookup {
|
||||
let Ok(lookup) = reader.lookup(ip) else {
|
||||
return GeoipLookup::default();
|
||||
};
|
||||
let Ok(Some(city)) = lookup.decode::<geoip2::City<'_>>() else {
|
||||
return GeoipLookup::default();
|
||||
};
|
||||
let country_code = city.country.iso_code.and_then(normalize_country_code);
|
||||
let region_code = city
|
||||
.subdivisions
|
||||
.first()
|
||||
.and_then(|s| s.iso_code)
|
||||
.map(|s| s.to_uppercase());
|
||||
let latitude = city.location.latitude;
|
||||
let longitude = city.location.longitude;
|
||||
GeoipLookup {
|
||||
country_code,
|
||||
region_code,
|
||||
latitude,
|
||||
longitude,
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_client_ip(
|
||||
headers: &HeaderMap,
|
||||
trust_client_ip_header: bool,
|
||||
header_name: &str,
|
||||
) -> Option<IpAddr> {
|
||||
if !trust_client_ip_header {
|
||||
return None;
|
||||
}
|
||||
let header_value = headers.get(header_name)?.to_str().ok()?;
|
||||
let first_hop = header_value.split(',').next()?.trim();
|
||||
parse_ip(first_hop)
|
||||
}
|
||||
|
||||
fn parse_ip(value: &str) -> Option<IpAddr> {
|
||||
if value.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if let Ok(ip) = value.parse() {
|
||||
return Some(ip);
|
||||
}
|
||||
if let Some(stripped) = value
|
||||
.strip_prefix('[')
|
||||
.and_then(|value| value.split_once(']').map(|(host, _)| host))
|
||||
{
|
||||
return stripped.parse().ok();
|
||||
}
|
||||
if value.matches(':').count() == 1
|
||||
&& let Some((host, _port)) = value.rsplit_once(':')
|
||||
{
|
||||
return host.parse().ok();
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn normalize_country_code(value: &str) -> Option<String> {
|
||||
let country_code = value.trim().to_ascii_uppercase();
|
||||
if country_code.len() == 2 && country_code.chars().all(|ch| ch.is_ascii_alphabetic()) {
|
||||
Some(country_code)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_geoip_database(
|
||||
source: &GeoipSourceConfig,
|
||||
s3_config: Option<&GeoipS3Config>,
|
||||
) -> anyhow::Result<Option<String>> {
|
||||
match source {
|
||||
GeoipSourceConfig::Filesystem { maxmind_db_path } => Ok(maxmind_db_path.clone()),
|
||||
GeoipSourceConfig::S3 {
|
||||
maxmind_db_path,
|
||||
s3_bucket,
|
||||
s3_key,
|
||||
} => {
|
||||
let s3 = s3_config.ok_or_else(|| {
|
||||
anyhow::anyhow!("GeoIP is configured for S3 mode, but S3 configuration is missing")
|
||||
})?;
|
||||
require_s3_config(s3)?;
|
||||
download_s3_object(s3, s3_bucket, s3_key, Path::new(maxmind_db_path))?;
|
||||
tracing::info!(
|
||||
maxmind_db_path,
|
||||
s3_bucket,
|
||||
s3_key,
|
||||
"GeoIP database downloaded from S3"
|
||||
);
|
||||
Ok(Some(maxmind_db_path.clone()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn require_s3_config(config: &GeoipS3Config) -> anyhow::Result<()> {
|
||||
require_s3_value(
|
||||
&config.endpoint,
|
||||
"GeoIP S3 startup mode requires s3.endpoint",
|
||||
)?;
|
||||
require_s3_value(&config.region, "GeoIP S3 startup mode requires s3.region")?;
|
||||
require_s3_value(
|
||||
&config.access_key_id,
|
||||
"GeoIP S3 startup mode requires s3.access_key_id",
|
||||
)?;
|
||||
require_s3_value(
|
||||
&config.secret_access_key,
|
||||
"GeoIP S3 startup mode requires s3.secret_access_key",
|
||||
)
|
||||
}
|
||||
|
||||
fn require_s3_value(value: &str, message: &str) -> anyhow::Result<()> {
|
||||
if value.trim().is_empty() {
|
||||
anyhow::bail!("{}", message);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn download_s3_object(
|
||||
config: &GeoipS3Config,
|
||||
bucket: &str,
|
||||
key: &str,
|
||||
destination: &Path,
|
||||
) -> anyhow::Result<()> {
|
||||
let Some(parent) = destination.parent() else {
|
||||
anyhow::bail!(
|
||||
"GeoIP S3 destination must include a parent directory: {}",
|
||||
destination.display()
|
||||
);
|
||||
};
|
||||
fs::create_dir_all(parent)?;
|
||||
let request = signed_s3_get_request(config, bucket, key)?;
|
||||
let mut response = reqwest::blocking::Client::new()
|
||||
.get(request.url)
|
||||
.headers(request.headers)
|
||||
.send()
|
||||
.map_err(|err| {
|
||||
anyhow::anyhow!("failed to download GeoIP database from s3://{bucket}/{key}: {err}")
|
||||
})?;
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
anyhow::bail!("failed to download GeoIP database from s3://{bucket}/{key}: HTTP {status}");
|
||||
}
|
||||
let mut file = tempfile::Builder::new().make_in(parent, |path| File::create_new(path))?;
|
||||
io::copy(&mut response, &mut file)?;
|
||||
file.persist(destination).map_err(|err| err.error)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct SignedS3Request {
|
||||
url: reqwest::Url,
|
||||
headers: reqwest::header::HeaderMap,
|
||||
}
|
||||
|
||||
fn signed_s3_get_request(
|
||||
config: &GeoipS3Config,
|
||||
bucket: &str,
|
||||
key: &str,
|
||||
) -> anyhow::Result<SignedS3Request> {
|
||||
let mut url = reqwest::Url::parse(config.endpoint.trim_end_matches('/'))?;
|
||||
{
|
||||
let mut segments = url.path_segments_mut().map_err(|_| {
|
||||
anyhow::anyhow!("S3 endpoint cannot be a base URL: {}", config.endpoint)
|
||||
})?;
|
||||
segments.pop_if_empty();
|
||||
segments.push(bucket);
|
||||
for segment in key.split('/') {
|
||||
segments.push(segment);
|
||||
}
|
||||
}
|
||||
|
||||
let identity = Credentials::new(
|
||||
config.access_key_id.clone(),
|
||||
config.secret_access_key.clone(),
|
||||
None,
|
||||
None,
|
||||
"fluxer-geoip",
|
||||
)
|
||||
.into();
|
||||
let mut signing_settings = SigningSettings::default();
|
||||
signing_settings.percent_encoding_mode = PercentEncodingMode::Single;
|
||||
signing_settings.payload_checksum_kind = PayloadChecksumKind::XAmzSha256;
|
||||
signing_settings.uri_path_normalization_mode = UriPathNormalizationMode::Disabled;
|
||||
let signing_params = v4::SigningParams::builder()
|
||||
.identity(&identity)
|
||||
.region(&config.region)
|
||||
.name("s3")
|
||||
.time(SystemTime::now())
|
||||
.settings(signing_settings)
|
||||
.build()?
|
||||
.into();
|
||||
let signable_request = SignableRequest::new(
|
||||
"GET",
|
||||
url.as_str(),
|
||||
std::iter::empty::<(&str, &str)>(),
|
||||
SignableBody::Bytes(&[]),
|
||||
)?;
|
||||
let (signing_instructions, _signature) = sign(signable_request, &signing_params)?.into_parts();
|
||||
let (signed_headers, signed_params) = signing_instructions.into_parts();
|
||||
if !signed_params.is_empty() {
|
||||
anyhow::bail!("S3 GeoIP signing unexpectedly produced query parameters for header signing");
|
||||
}
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
for header in signed_headers {
|
||||
let name = reqwest::header::HeaderName::from_bytes(header.name().as_bytes())?;
|
||||
let mut value = reqwest::header::HeaderValue::from_str(header.value())?;
|
||||
value.set_sensitive(header.sensitive());
|
||||
headers.insert(name, value);
|
||||
}
|
||||
Ok(SignedS3Request { url, headers })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use axum::http::HeaderValue;
|
||||
|
||||
#[test]
|
||||
fn extracts_first_configured_client_ip_only_when_trusted() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
"x-forwarded-for",
|
||||
HeaderValue::from_static("203.0.113.10, 10.0.0.4"),
|
||||
);
|
||||
assert_eq!(
|
||||
extract_client_ip(&headers, true, "x-forwarded-for"),
|
||||
Some("203.0.113.10".parse().unwrap())
|
||||
);
|
||||
assert_eq!(extract_client_ip(&headers, false, "x-forwarded-for"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ip_handles_ipv4() {
|
||||
assert_eq!(
|
||||
parse_ip("203.0.113.10"),
|
||||
Some("203.0.113.10".parse().unwrap())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ip_handles_ipv4_with_port() {
|
||||
assert_eq!(
|
||||
parse_ip("203.0.113.10:8080"),
|
||||
Some("203.0.113.10".parse().unwrap())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ip_handles_ipv6_bracketed() {
|
||||
assert_eq!(parse_ip("[::1]:8080"), Some("::1".parse().unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ip_handles_empty() {
|
||||
assert_eq!(parse_ip(""), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalizes_valid_country_codes() {
|
||||
assert_eq!(normalize_country_code("se"), Some("SE".to_owned()));
|
||||
assert_eq!(normalize_country_code("US"), Some("US".to_owned()));
|
||||
assert_eq!(normalize_country_code("abc"), None);
|
||||
assert_eq!(normalize_country_code(""), None);
|
||||
}
|
||||
}
|
||||
@@ -3,5 +3,4 @@
|
||||
pub mod attachment_url_signature;
|
||||
pub mod config;
|
||||
pub mod external_media_path;
|
||||
pub mod geoip;
|
||||
pub mod user_flags;
|
||||
Reference in new issue
Block a user