use crate::error::CliError;
use crate::platform;
use rand::seq::{IteratorRandom, SliceRandom};
use reqwest::{
header::{
HeaderMap, HeaderName, HeaderValue, ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, CACHE_CONTROL,
},
redirect::Policy,
Client,
};
use serde::Deserialize;
use std::time::Duration;
const USER_AGENTS_DEFAULT: &[&str] = &[
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 Edg/145.0.3800.97",
"Mozilla/5.0 (X11; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.15",
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BrowserFamily {
Chrome,
Firefox,
Safari,
Edge,
}
#[derive(Debug, Clone)]
pub struct BrowserProfile {
pub family: BrowserFamily,
pub user_agent: String,
pub major_version: u32,
pub ua_platform: &'static str,
}
pub fn detect_family(ua: &str) -> BrowserFamily {
if ua.contains("Edg/") {
BrowserFamily::Edge
} else if ua.contains("Chrome/") {
BrowserFamily::Chrome
} else if ua.contains("Firefox/") {
BrowserFamily::Firefox
} else if ua.contains("Safari/") {
BrowserFamily::Safari
} else {
BrowserFamily::Firefox
}
}
fn extract_major_version(ua: &str, family: BrowserFamily) -> u32 {
let prefix = match family {
BrowserFamily::Chrome => "Chrome/",
BrowserFamily::Firefox => "Firefox/",
BrowserFamily::Safari => "Version/",
BrowserFamily::Edge => "Edg/",
};
if let Some(pos) = ua.find(prefix) {
let rest = &ua[pos + prefix.len()..];
return rest
.chars()
.take_while(|c| c.is_ascii_digit())
.fold(0u32, |acc, c| acc * 10 + c.to_digit(10).unwrap_or(0));
}
0
}
fn extract_ua_platform(ua: &str) -> &'static str {
if ua.contains("Windows NT") {
"Windows"
} else if ua.contains("Macintosh") {
"macOS"
} else {
"Linux"
}
}
pub fn create_browser_profile(ua: &str) -> BrowserProfile {
let family = detect_family(ua);
let major_version = extract_major_version(ua, family);
let ua_platform = extract_ua_platform(ua);
BrowserProfile {
family,
user_agent: ua.to_string(),
major_version,
ua_platform,
}
}
impl BrowserProfile {
pub fn initial_headers(&self, language: &str, country: &str) -> Result<HeaderMap, CliError> {
let mut headers = HeaderMap::new();
let accept_value = match self.family {
BrowserFamily::Chrome | BrowserFamily::Edge => {
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8"
}
BrowserFamily::Firefox => {
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"
}
BrowserFamily::Safari => {
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
}
};
headers.insert(ACCEPT, HeaderValue::from_static(accept_value));
let language_lower = language.to_ascii_lowercase();
let country_upper = country.to_ascii_uppercase();
let accept_language = if language_lower == "en" {
"en-US,en;q=0.9".to_string()
} else {
format!("{language_lower}-{country_upper},{language_lower};q=0.9,en-US;q=0.8,en;q=0.7")
};
headers.insert(
ACCEPT_LANGUAGE,
HeaderValue::from_str(&accept_language).map_err(|e| CliError::InvalidConfig {
message: format!("Accept-Language contains invalid characters: {e}"),
})?,
);
headers.insert(
ACCEPT_ENCODING,
HeaderValue::from_static("gzip, deflate, br"),
);
headers.insert(
HeaderName::from_static("upgrade-insecure-requests"),
HeaderValue::from_static("1"),
);
headers.insert(
HeaderName::from_static("sec-fetch-dest"),
HeaderValue::from_static("document"),
);
headers.insert(
HeaderName::from_static("sec-fetch-mode"),
HeaderValue::from_static("navigate"),
);
headers.insert(
HeaderName::from_static("sec-fetch-site"),
HeaderValue::from_static("none"),
);
headers.insert(
HeaderName::from_static("sec-fetch-user"),
HeaderValue::from_static("?1"),
);
if matches!(self.family, BrowserFamily::Chrome | BrowserFamily::Edge) {
let sec_ch_ua = match self.family {
BrowserFamily::Edge => format!(
r#""Chromium";v="{v}", "Microsoft Edge";v="{v}", "Not-A.Brand";v="99""#,
v = self.major_version
),
_ => format!(
r#""Chromium";v="{v}", "Google Chrome";v="{v}", "Not-A.Brand";v="99""#,
v = self.major_version
),
};
headers.insert(
HeaderName::from_static("sec-ch-ua"),
HeaderValue::from_str(&sec_ch_ua).map_err(|e| CliError::InvalidConfig {
message: format!("Sec-CH-UA contains invalid characters: {e}"),
})?,
);
headers.insert(
HeaderName::from_static("sec-ch-ua-mobile"),
HeaderValue::from_static("?0"),
);
let platform_quoted = format!(r#""{}""#, self.ua_platform);
headers.insert(
HeaderName::from_static("sec-ch-ua-platform"),
HeaderValue::from_str(&platform_quoted).map_err(|e| CliError::InvalidConfig {
message: format!("Sec-CH-UA-Platform contains invalid characters: {e}"),
})?,
);
headers.insert(CACHE_CONTROL, HeaderValue::from_static("max-age=0"));
}
Ok(headers)
}
pub fn pagination_headers(&self) -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert(
HeaderName::from_static("sec-fetch-dest"),
HeaderValue::from_static("document"),
);
headers.insert(
HeaderName::from_static("sec-fetch-mode"),
HeaderValue::from_static("navigate"),
);
headers.insert(
HeaderName::from_static("sec-fetch-site"),
HeaderValue::from_static("same-origin"),
);
headers.insert(
HeaderName::from_static("sec-fetch-user"),
HeaderValue::from_static("?1"),
);
headers
}
}
#[derive(Debug, Clone, Deserialize)]
struct ExternalTomlAgent {
ua: String,
#[serde(default = "platform_any")]
platform: String,
#[serde(default)]
#[allow(dead_code)]
browser: Option<String>,
}
fn platform_any() -> String {
"any".to_string()
}
#[derive(Debug, Clone, Deserialize)]
struct UserAgentsFile {
#[serde(default)]
agents: Vec<ExternalTomlAgent>,
}
pub fn load_user_agents(match_platform: bool) -> Vec<String> {
let Some(path) = platform::user_agents_toml_path() else {
tracing::debug!("no config directory — using built-in UAs");
return default_user_agents_vec();
};
if path.metadata().map(|m| m.len()).unwrap_or(0) > 1_048_576 {
tracing::warn!(
path = %path.display(),
"user-agents.toml exceeds 1 MB limit — using built-in UAs"
);
return default_user_agents_vec();
}
let content = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(err) => {
tracing::info!(
path = %path.display(),
?err,
"user-agents.toml not found — using built-in UAs"
);
return default_user_agents_vec();
}
};
let file_data: UserAgentsFile = match toml::from_str(&content) {
Ok(a) => a,
Err(err) => {
tracing::warn!(
path = %path.display(),
?err,
"user-agents.toml invalid — using built-in UAs"
);
return default_user_agents_vec();
}
};
let current_platform = platform::platform_name();
let filtered: Vec<String> = file_data
.agents
.into_iter()
.filter(|a| {
if !match_platform {
return true;
}
a.platform == "any" || a.platform == current_platform
})
.map(|a| a.ua)
.filter(|ua| !ua.is_empty())
.collect();
if filtered.is_empty() {
tracing::warn!("user-agents.toml produced no applicable UA — using defaults");
return default_user_agents_vec();
}
tracing::info!(
path = %path.display(),
total = filtered.len(),
match_platform,
"User-Agents loaded from external user-agents.toml"
);
filtered
}
fn default_user_agents_vec() -> Vec<String> {
use std::sync::OnceLock;
static CACHE: OnceLock<Vec<String>> = OnceLock::new();
CACHE
.get_or_init(|| {
USER_AGENTS_DEFAULT
.iter()
.map(|s| (*s).to_string())
.collect()
})
.clone()
}
pub fn select_user_agent() -> String {
let mut rng = rand::thread_rng();
USER_AGENTS_DEFAULT
.choose(&mut rng)
.copied()
.unwrap_or(USER_AGENTS_DEFAULT[0])
.to_string()
}
pub fn select_user_agent_from_list(list: &[String]) -> String {
let mut rng = rand::thread_rng();
list.choose(&mut rng)
.cloned()
.unwrap_or_else(select_user_agent)
}
pub fn select_profile_from_list(list: &[String]) -> BrowserProfile {
let ua = select_user_agent_from_list(list);
create_browser_profile(&ua)
}
pub fn select_profile_from_list_seeded(list: &[String], seed: Option<u64>) -> BrowserProfile {
match seed {
Some(s) => {
use rand::SeedableRng;
let mut rng = rand::rngs::StdRng::seed_from_u64(s);
let ua = if list.is_empty() {
USER_AGENTS_DEFAULT
.choose(&mut rng)
.copied()
.unwrap_or(USER_AGENTS_DEFAULT[0])
.to_string()
} else {
list.choose(&mut rng)
.cloned()
.unwrap_or_else(select_user_agent)
};
create_browser_profile(&ua)
}
None => select_profile_from_list(list),
}
}
pub fn select_random_user_agent(excluding: Option<&str>) -> String {
let mut rng = rand::thread_rng();
let chosen = USER_AGENTS_DEFAULT
.iter()
.filter(|ua| match excluding {
Some(excl) => **ua != excl,
None => true,
})
.choose(&mut rng);
match chosen {
Some(ua) => ua.to_string(),
None => select_user_agent(),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProxyConfig {
Unset,
Disabled,
Url(String),
}
impl ProxyConfig {
pub fn from_options(proxy: Option<&str>, no_proxy: bool) -> Self {
if no_proxy {
return Self::Disabled;
}
match proxy {
Some(u) if !u.is_empty() => Self::Url(u.to_string()),
_ => Self::Unset,
}
}
pub fn is_active(&self) -> bool {
matches!(self, Self::Url(_))
}
}
pub fn build_client(
user_agent: &str,
timeout_secs: u64,
language: &str,
country: &str,
) -> Result<Client, CliError> {
let profile = create_browser_profile(user_agent);
build_client_with_proxy(
&profile,
timeout_secs,
language,
country,
&ProxyConfig::Unset,
)
}
fn mask_proxy_url(raw_url: &str) -> String {
match reqwest::Url::parse(raw_url) {
Ok(parsed) => {
let user = parsed.username();
let has_password = parsed.password().is_some();
if user.is_empty() && !has_password {
return format!(
"{}://{}{}",
parsed.scheme(),
parsed.host_str().unwrap_or("?"),
parsed.port().map(|p| format!(":{p}")).unwrap_or_default()
);
}
let masked_user = if user.len() > 2 {
format!("{}***", &user[..2])
} else {
format!("{user}***")
};
format!(
"{}://{}@{}{}",
parsed.scheme(),
masked_user,
parsed.host_str().unwrap_or("?"),
parsed.port().map(|p| format!(":{p}")).unwrap_or_default()
)
}
Err(_) => "***URL_MALFORMADA***".to_string(),
}
}
pub fn build_client_with_proxy(
profile: &BrowserProfile,
timeout_secs: u64,
language: &str,
country: &str,
proxy: &ProxyConfig,
) -> Result<Client, CliError> {
let headers = profile.initial_headers(language, country)?;
let mut builder = Client::builder()
.user_agent(&profile.user_agent)
.default_headers(headers)
.cookie_store(true)
.tcp_nodelay(true)
.tcp_keepalive(Duration::from_secs(60))
.pool_max_idle_per_host(10)
.connect_timeout(Duration::from_secs(10))
.gzip(true)
.brotli(true)
.redirect(Policy::limited(5))
.timeout(Duration::from_secs(timeout_secs));
match proxy {
ProxyConfig::Unset => {}
ProxyConfig::Disabled => {
builder = builder.no_proxy();
tracing::info!("proxy explicitly disabled via --no-proxy");
}
ProxyConfig::Url(url) => {
let parsed_url = reqwest::Url::parse(url).map_err(|e| CliError::ProxyError {
message: format!("invalid proxy URL {}: {e}", mask_proxy_url(url)),
})?;
let user = parsed_url.username().to_string();
let password = parsed_url
.password()
.map(|s| s.to_string())
.unwrap_or_default();
let mut proxy_rq = reqwest::Proxy::all(url).map_err(|e| CliError::ProxyError {
message: format!(
"failed to configure Proxy::all({}): {e}",
mask_proxy_url(url)
),
})?;
if !user.is_empty() {
proxy_rq = proxy_rq.basic_auth(&user, &password);
}
builder = builder.proxy(proxy_rq);
tracing::info!(
host = parsed_url.host_str(),
scheme = parsed_url.scheme(),
"proxy configured"
);
}
}
let client = builder.build().map_err(|e| CliError::HttpError {
message: format!("failed to build reqwest::Client: {e}"),
cause: None,
})?;
Ok(client)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn choose_user_agent_returns_non_empty_string() {
let ua = select_user_agent();
assert!(!ua.is_empty());
}
#[test]
fn choose_user_agent_returns_modern_ua_from_pool() {
let ua = select_user_agent();
assert!(
USER_AGENTS_DEFAULT.contains(&ua.as_str()),
"UA selecionado deve estar na lista padrão: {ua}"
);
assert!(
ua.starts_with("Mozilla/5.0 ("),
"UAs padrão v0.3.0 iniciam com 'Mozilla/5.0 (' (browser real): {ua}"
);
}
#[test]
fn default_pool_contains_modern_browsers_in_all_families() {
let pool = USER_AGENTS_DEFAULT;
assert!(pool.iter().any(|ua| ua.contains("Chrome/")));
assert!(pool.iter().any(|ua| ua.contains("Firefox/")));
assert!(pool.iter().any(|ua| ua.contains("Edg/")));
assert!(pool
.iter()
.any(|ua| ua.contains("Safari/") && !ua.contains("Chrome/")));
}
#[test]
fn default_pool_does_not_contain_removed_text_browsers() {
for ua in USER_AGENTS_DEFAULT {
assert!(!ua.contains("Lynx"), "UA banido detectado (Lynx): {ua}");
assert!(!ua.contains("w3m"), "UA banido detectado (w3m): {ua}");
assert!(
!ua.starts_with("Links ("),
"UA banido detectado (Links): {ua}"
);
assert!(!ua.contains("ELinks"), "UA banido detectado (ELinks): {ua}");
assert!(
!ua.starts_with("duckduckgo-search-cli"),
"UA banido detectado (self-cli): {ua}"
);
assert_ne!(
*ua, "Mozilla/5.0",
"UA minimalista 'Mozilla/5.0' deve ter sido removido"
);
}
assert!(!USER_AGENTS_DEFAULT.is_empty());
}
#[test]
fn select_random_user_agent_without_exclusion_returns_valid() {
let ua = select_random_user_agent(None);
assert!(!ua.is_empty());
}
#[test]
fn select_random_user_agent_avoids_excluded_when_possible() {
let excluded = USER_AGENTS_DEFAULT[0];
for _ in 0..20 {
let ua = select_random_user_agent(Some(excluded));
assert_ne!(ua, excluded);
assert!(!ua.is_empty());
}
}
#[test]
fn build_client_with_valid_values_works() {
let client = build_client("Mozilla/5.0 teste", 15, "pt", "br");
assert!(client.is_ok(), "cliente deve ser construído sem erro");
}
#[test]
fn build_client_with_http_proxy_works() {
let profile = create_browser_profile("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36");
let proxy = ProxyConfig::Url("http://user:pass@proxy.local:8080".to_string());
let client = build_client_with_proxy(&profile, 10, "pt", "br", &proxy);
assert!(client.is_ok(), "client with HTTP proxy should build");
}
#[test]
fn build_client_with_socks5_proxy_works() {
let profile = create_browser_profile(
"Mozilla/5.0 (X11; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0",
);
let proxy = ProxyConfig::Url("socks5://127.0.0.1:9050".to_string());
let client = build_client_with_proxy(&profile, 10, "pt", "br", &proxy);
assert!(client.is_ok(), "client with SOCKS5 should build");
}
#[test]
fn build_client_with_no_proxy_works() {
let profile = create_browser_profile(
"Mozilla/5.0 (X11; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0",
);
let proxy = ProxyConfig::Disabled;
let client = build_client_with_proxy(&profile, 10, "pt", "br", &proxy);
assert!(client.is_ok(), "client with no_proxy should build");
}
#[test]
fn build_client_with_invalid_proxy_url_fails() {
let profile = create_browser_profile(
"Mozilla/5.0 (X11; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0",
);
let proxy = ProxyConfig::Url("nao eh uma url".to_string());
let client = build_client_with_proxy(&profile, 10, "pt", "br", &proxy);
assert!(client.is_err(), "invalid URL should be rejected");
}
#[test]
fn proxy_config_from_flags() {
assert_eq!(ProxyConfig::from_options(None, false), ProxyConfig::Unset);
assert_eq!(ProxyConfig::from_options(None, true), ProxyConfig::Disabled);
assert_eq!(
ProxyConfig::from_options(Some("http://x:9"), false),
ProxyConfig::Url("http://x:9".to_string())
);
assert_eq!(
ProxyConfig::from_options(Some("http://x:9"), true),
ProxyConfig::Disabled
);
}
#[test]
fn proxy_config_is_active_only_for_url() {
assert!(!ProxyConfig::Unset.is_active());
assert!(!ProxyConfig::Disabled.is_active());
assert!(ProxyConfig::Url("http://x".to_string()).is_active());
}
#[test]
fn mask_proxy_url_with_credentials() {
let result = mask_proxy_url("http://admin:s3cret@proxy.local:8080");
assert!(!result.contains("s3cret"), "password vazou: {result}");
assert!(
!result.contains("admin"),
"username completo vazou: {result}"
);
assert!(
result.contains("ad***"),
"username mascarado ausente: {result}"
);
assert!(result.contains("proxy.local"));
assert!(result.contains("8080"));
}
#[test]
fn mask_proxy_url_without_credentials() {
let result = mask_proxy_url("http://proxy.local:8080");
assert_eq!(result, "http://proxy.local:8080");
}
#[test]
fn mask_proxy_url_username_only() {
let result = mask_proxy_url("http://user@proxy.local:3128");
assert!(result.contains("us***"));
assert!(!result.contains("user@"));
}
#[test]
fn mask_proxy_url_malformed() {
let result = mask_proxy_url("not-a-url");
assert_eq!(result, "***URL_MALFORMADA***");
}
#[test]
fn mask_proxy_url_socks5() {
let result = mask_proxy_url("socks5://root:toor@127.0.0.1:1080");
assert!(!result.contains("toor"));
assert!(result.contains("socks5://"));
assert!(result.contains("127.0.0.1"));
}
#[test]
fn mask_proxy_url_short_username() {
let result = mask_proxy_url("http://a:pass@proxy:80");
assert!(result.contains("a***"));
assert!(!result.contains("pass"));
}
#[test]
fn load_user_agents_returns_at_least_one_default() {
let agents = load_user_agents(false);
assert!(!agents.is_empty());
for ua in &agents {
assert!(!ua.is_empty());
}
}
#[test]
fn choose_user_agent_from_list_returns_list_item() {
let agents = vec!["A".to_string(), "B".to_string(), "C".to_string()];
for _ in 0..10 {
let selected = select_user_agent_from_list(&agents);
assert!(agents.contains(&selected));
}
}
#[test]
fn detect_family_chrome() {
let uas_chrome = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
];
for ua in &uas_chrome {
assert_eq!(
detect_family(ua),
BrowserFamily::Chrome,
"esperado Chrome para: {ua}"
);
}
}
#[test]
fn detect_family_edge_before_chrome() {
let ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 Edg/145.0.3800.97";
assert_eq!(detect_family(ua), BrowserFamily::Edge);
}
#[test]
fn detect_family_firefox() {
let ua = "Mozilla/5.0 (X11; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0";
assert_eq!(detect_family(ua), BrowserFamily::Firefox);
}
#[test]
fn detect_family_safari() {
let ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.15";
assert_eq!(detect_family(ua), BrowserFamily::Safari);
}
#[test]
fn extract_major_version_chrome_146() {
let ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36";
let version = extract_major_version(ua, BrowserFamily::Chrome);
assert_eq!(version, 146, "versão major Chrome deve ser 146");
}
#[test]
fn extract_major_version_firefox_134() {
let ua = "Mozilla/5.0 (X11; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0";
let version = extract_major_version(ua, BrowserFamily::Firefox);
assert_eq!(version, 134, "versão major Firefox deve ser 134");
}
#[test]
fn initial_chrome_headers_include_sec_fetch() {
let ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36";
let profile = create_browser_profile(ua);
let headers = profile
.initial_headers("pt", "br")
.expect("should build headers");
assert!(
headers.contains_key("sec-fetch-dest"),
"sec-fetch-dest ausente"
);
assert!(
headers.contains_key("sec-fetch-mode"),
"sec-fetch-mode ausente"
);
assert!(
headers.contains_key("sec-fetch-site"),
"sec-fetch-site ausente"
);
assert!(
headers.contains_key("sec-fetch-user"),
"sec-fetch-user ausente"
);
}
#[test]
fn initial_chrome_headers_include_client_hints() {
let ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36";
let profile = create_browser_profile(ua);
let headers = profile
.initial_headers("pt", "br")
.expect("should build headers");
assert!(headers.contains_key("sec-ch-ua"), "sec-ch-ua ausente");
assert!(
headers.contains_key("sec-ch-ua-mobile"),
"sec-ch-ua-mobile ausente"
);
assert!(
headers.contains_key("sec-ch-ua-platform"),
"sec-ch-ua-platform ausente"
);
}
#[test]
fn initial_firefox_headers_omit_client_hints() {
let ua = "Mozilla/5.0 (X11; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0";
let profile = create_browser_profile(ua);
let headers = profile
.initial_headers("pt", "br")
.expect("should build headers");
assert!(
!headers.contains_key("sec-ch-ua"),
"Firefox NÃO deve ter sec-ch-ua"
);
assert!(
!headers.contains_key("sec-ch-ua-mobile"),
"Firefox NÃO deve ter sec-ch-ua-mobile"
);
assert!(
!headers.contains_key("sec-ch-ua-platform"),
"Firefox NÃO deve ter sec-ch-ua-platform"
);
}
#[test]
fn pagination_headers_sec_fetch_site_same_origin() {
let ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36";
let profile = create_browser_profile(ua);
let headers = profile.pagination_headers();
let value = headers
.get("sec-fetch-site")
.expect("sec-fetch-site should be present");
assert_eq!(value.to_str().unwrap(), "same-origin");
}
#[test]
fn accept_language_with_q_values_pt() {
let ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36";
let profile = create_browser_profile(ua);
let headers = profile
.initial_headers("pt", "br")
.expect("should build headers");
let al = headers
.get(ACCEPT_LANGUAGE)
.expect("Accept-Language present");
let al_str = al.to_str().unwrap();
assert!(al_str.contains("pt-BR"), "deve conter pt-BR: {al_str}");
assert!(
al_str.contains("pt;q=0.9"),
"deve conter pt;q=0.9: {al_str}"
);
assert!(
al_str.contains("en-US;q=0.8"),
"deve conter en-US;q=0.8: {al_str}"
);
assert!(
al_str.contains("en;q=0.7"),
"deve conter en;q=0.7: {al_str}"
);
}
#[test]
fn accept_language_with_q_values_en() {
let ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36";
let profile = create_browser_profile(ua);
let headers = profile
.initial_headers("en", "us")
.expect("should build headers");
let al = headers
.get(ACCEPT_LANGUAGE)
.expect("Accept-Language present");
let al_str = al.to_str().unwrap();
assert_eq!(
al_str, "en-US,en;q=0.9",
"formato en deve ser simplificado: {al_str}"
);
}
#[test]
fn default_headers_include_accept_and_language() {
let ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36";
let profile = create_browser_profile(ua);
let headers = profile
.initial_headers("pt", "br")
.expect("should build headers");
let accept = headers.get(ACCEPT).expect("ACCEPT present");
assert!(accept.to_str().unwrap().contains("text/html"));
let al = headers
.get(ACCEPT_LANGUAGE)
.expect("ACCEPT_LANGUAGE present");
assert!(al.to_str().unwrap().contains("pt-BR"));
}
#[test]
fn default_headers_omit_dnt_and_referer() {
let ua = "Mozilla/5.0 (X11; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0";
let profile = create_browser_profile(ua);
let headers = profile
.initial_headers("en", "us")
.expect("should build headers");
assert!(headers.get(reqwest::header::DNT).is_none());
assert!(headers.get(reqwest::header::REFERER).is_none());
}
}