use std::future::Future;
use super::CookieSource;
use crate::content::response_classifier::{ResponseDiagnosticKind, classify_http_response};
pub const FALLBACK_ORDER: [CookieSource; 4] = [
CookieSource::Brave,
CookieSource::Chrome,
CookieSource::Firefox,
CookieSource::Safari,
];
#[must_use]
pub fn fallback_candidates(already_tried: CookieSource) -> Vec<CookieSource> {
FALLBACK_ORDER
.iter()
.copied()
.filter(|source| *source != already_tried)
.collect()
}
#[must_use]
pub fn is_challenge(status: u16, body: &str) -> bool {
matches!(
classify_http_response(status, body).map(|d| d.kind),
Some(ResponseDiagnosticKind::BrowserChallenge(_))
)
}
pub enum AttemptOutcome {
Available { status: u16, body: String },
Unavailable,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FallbackResult {
Resolved {
source: CookieSource,
status: u16,
body: String,
},
Exhausted { tried: Vec<CookieSource> },
}
pub async fn fallback_over_profiles<F, Fut>(
candidates: Vec<CookieSource>,
mut attempt: F,
) -> FallbackResult
where
F: FnMut(CookieSource) -> Fut,
Fut: Future<Output = AttemptOutcome>,
{
let mut tried = Vec::new();
for source in candidates {
match attempt(source).await {
AttemptOutcome::Unavailable => {}
AttemptOutcome::Available { status, body } => {
tried.push(source);
if !is_challenge(status, &body) {
return FallbackResult::Resolved {
source,
status,
body,
};
}
}
}
}
FallbackResult::Exhausted { tried }
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::{
AttemptOutcome, CookieSource, FallbackResult, fallback_candidates, fallback_over_profiles,
is_challenge,
};
const CHALLENGE_BODY: &str = "<html><head><title>Just a moment...</title></head><body>\
<div id='challenge-error-text'>Enable JavaScript and cookies to continue</div>\
<div id='cf-chl-widget'></div></body></html>";
const CLEAN_BODY: &str =
"<html><body><article><h1>Real Article</h1><p>Lots of words.</p></article></body></html>";
#[test]
fn challenge_body_with_cloudflare_markers_is_detected() {
assert!(is_challenge(403, CHALLENGE_BODY));
}
#[test]
fn clean_article_is_not_a_challenge() {
assert!(!is_challenge(200, CLEAN_BODY));
}
#[test]
fn fallback_candidates_excludes_already_tried_and_keeps_order() {
let candidates = fallback_candidates(CookieSource::Brave);
assert_eq!(
candidates,
vec![
CookieSource::Chrome,
CookieSource::Firefox,
CookieSource::Safari
]
);
}
#[tokio::test]
async fn fallback_returns_first_clean_profile_body() {
let mut table: HashMap<CookieSource, (u16, &str)> = HashMap::new();
table.insert(CookieSource::Chrome, (403, CHALLENGE_BODY));
table.insert(CookieSource::Firefox, (200, CLEAN_BODY));
table.insert(CookieSource::Safari, (200, CLEAN_BODY));
let result = fallback_over_profiles(fallback_candidates(CookieSource::Brave), |source| {
let entry = table.get(&source).copied();
async move {
match entry {
Some((status, body)) => AttemptOutcome::Available {
status,
body: body.to_string(),
},
None => AttemptOutcome::Unavailable,
}
}
})
.await;
match result {
FallbackResult::Resolved { source, body, .. } => {
assert_eq!(source, CookieSource::Firefox);
assert_eq!(body, CLEAN_BODY);
}
FallbackResult::Exhausted { .. } => panic!("expected a clean profile to resolve"),
}
}
#[tokio::test]
async fn unavailable_profiles_are_skipped_not_counted() {
let result = fallback_over_profiles(fallback_candidates(CookieSource::Brave), |source| {
let outcome = match source {
CookieSource::Firefox => AttemptOutcome::Available {
status: 200,
body: CLEAN_BODY.to_string(),
},
_ => AttemptOutcome::Unavailable,
};
async move { outcome }
})
.await;
assert_eq!(
result,
FallbackResult::Resolved {
source: CookieSource::Firefox,
status: 200,
body: CLEAN_BODY.to_string(),
}
);
}
#[tokio::test]
async fn all_profiles_challenged_returns_exhausted_with_tried_list() {
let result = fallback_over_profiles(fallback_candidates(CookieSource::Brave), |source| {
let outcome = match source {
CookieSource::Safari => AttemptOutcome::Unavailable,
_ => AttemptOutcome::Available {
status: 403,
body: CHALLENGE_BODY.to_string(),
},
};
async move { outcome }
})
.await;
assert_eq!(
result,
FallbackResult::Exhausted {
tried: vec![CookieSource::Chrome, CookieSource::Firefox],
}
);
}
}