use std::time::Duration;
use async_trait::async_trait;
use serde_json::Value;
use tail_fin_core::{
AuthFailureKind, BrowserSession, FailureIndicators, SessionStatus, Site, SiteError,
};
pub struct FiveNineOneSite;
#[async_trait]
impl Site for FiveNineOneSite {
fn id(&self) -> &'static str {
"591"
}
fn display_name(&self) -> &'static str {
"591 Taiwan Rentals"
}
fn cookie_domain_patterns(&self) -> &'static [&'static str] {
&["*.591.com.tw"]
}
fn refresh_url(&self) -> &'static str {
"https://www.591.com.tw/"
}
fn refresh_interval_min(&self) -> Duration {
Duration::from_secs(3600)
}
async fn refresh(&self, _session: &BrowserSession) -> Result<Vec<Value>, SiteError> {
Ok(vec![])
}
async fn validate(&self, session: &BrowserSession) -> Result<SessionStatus, SiteError> {
let status = session
.http_ping("https://www.591.com.tw/")
.await
.map_err(|e| SiteError::ValidationFailed {
site: self.id(),
reason: format!("homepage ping: {e}"),
})?;
Ok(match status {
200 => SessionStatus::Valid,
401 | 403 => SessionStatus::Expired,
429 => SessionStatus::Blocked {
reason: "591 rate limit".into(),
retry_after: Some(Duration::from_secs(180)),
},
0 => SessionStatus::Unknown,
other => SessionStatus::Degrading {
estimated_expiry: None,
hint: format!("unexpected HTTP {other}"),
},
})
}
fn detect_auth_failure(&self, indicators: &FailureIndicators) -> Option<AuthFailureKind> {
match indicators.status {
Some(401) | Some(403) => Some(AuthFailureKind::CookieExpired),
Some(429) => Some(AuthFailureKind::RateLimited {
retry_after: Duration::from_secs(180),
}),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn indicators(status: u16) -> FailureIndicators {
FailureIndicators {
status: Some(status),
..Default::default()
}
}
#[test]
fn identity_fields() {
let s = FiveNineOneSite;
assert_eq!(s.id(), "591");
assert_eq!(s.display_name(), "591 Taiwan Rentals");
assert_eq!(s.cookie_domain_patterns(), &["*.591.com.tw"]);
assert_eq!(s.refresh_url(), "https://www.591.com.tw/");
}
#[test]
fn refresh_interval_is_one_hour() {
assert_eq!(
FiveNineOneSite.refresh_interval_min(),
Duration::from_secs(3600)
);
}
#[test]
fn detect_cookie_expired_on_401_and_403() {
for status in [401, 403] {
assert!(matches!(
FiveNineOneSite.detect_auth_failure(&indicators(status)),
Some(AuthFailureKind::CookieExpired)
));
}
}
#[test]
fn detect_rate_limited_on_429() {
match FiveNineOneSite.detect_auth_failure(&indicators(429)) {
Some(AuthFailureKind::RateLimited { retry_after }) => {
assert_eq!(retry_after, Duration::from_secs(180));
}
other => panic!("expected RateLimited, got {other:?}"),
}
}
#[test]
fn detect_returns_none_for_ok_and_unknown() {
assert!(FiveNineOneSite
.detect_auth_failure(&indicators(200))
.is_none());
assert!(FiveNineOneSite
.detect_auth_failure(&indicators(500))
.is_none());
}
}