use async_trait::async_trait;
use serde_json::Value;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::mpsc;
use night_fury_core::{BrowserCmd, BrowserSession};
use tail_fin_core::{
AuthFailureKind, FailureIndicators, SessionManager, SessionStatus, Site, SiteError,
};
struct CountingSite {
refresh_calls: AtomicUsize,
}
impl CountingSite {
fn new() -> Self {
Self {
refresh_calls: AtomicUsize::new(0),
}
}
fn refresh_count(&self) -> usize {
self.refresh_calls.load(Ordering::SeqCst)
}
}
#[async_trait]
impl Site for CountingSite {
fn id(&self) -> &'static str {
"rec"
}
fn display_name(&self) -> &'static str {
"Recording Site"
}
fn cookie_domain_patterns(&self) -> &'static [&'static str] {
&["*.rec.test"]
}
fn refresh_url(&self) -> &'static str {
"https://rec.test/"
}
fn refresh_interval_min(&self) -> Duration {
Duration::from_millis(1)
}
async fn refresh(&self, _session: &BrowserSession) -> Result<Vec<Value>, SiteError> {
self.refresh_calls.fetch_add(1, Ordering::SeqCst);
Ok(vec![serde_json::json!({
"name": "fresh_token",
"value": "v1",
"domain": ".rec.test"
})])
}
async fn validate(&self, _session: &BrowserSession) -> Result<SessionStatus, SiteError> {
Ok(SessionStatus::Valid)
}
fn detect_auth_failure(&self, _ind: &FailureIndicators) -> Option<AuthFailureKind> {
None
}
}
#[derive(Default)]
struct FakeWorker {
cmds_seen: Arc<AtomicUsize>,
}
impl FakeWorker {
fn new() -> Self {
Self::default()
}
fn cmds_seen(&self) -> Arc<AtomicUsize> {
self.cmds_seen.clone()
}
fn spawn(self, mut rx: mpsc::Receiver<BrowserCmd>) -> tokio::task::JoinHandle<()> {
let counter = self.cmds_seen.clone();
tokio::spawn(async move {
while let Some(_cmd) = rx.recv().await {
counter.fetch_add(1, Ordering::SeqCst);
}
})
}
}
fn setup() -> (SessionManager, Arc<CountingSite>, Arc<AtomicUsize>) {
let (tx, rx) = mpsc::channel(8);
let worker = FakeWorker::new();
let cmds_seen = worker.cmds_seen();
let _handle = worker.spawn(rx);
let browser = BrowserSession::from_sender(tx);
let site = Arc::new(CountingSite::new());
let manager = SessionManager::new(site.clone(), browser);
(manager, site, cmds_seen)
}
#[tokio::test]
async fn refresh_with_seed_empty_delegates_to_refresh() {
let (manager, site, cmds_seen) = setup();
let cookies = manager
.refresh_with_seed(&[])
.await
.expect("refresh_with_seed failed");
assert_eq!(site.refresh_count(), 1);
assert_eq!(cookies.len(), 1);
assert_eq!(cmds_seen.load(Ordering::SeqCst), 0);
let snapshot = manager.cookies().await;
assert_eq!(snapshot.len(), 1);
assert_eq!(snapshot[0]["name"], "fresh_token");
}
#[tokio::test]
async fn refresh_with_seed_nonempty_calls_set_cookies_before_refresh() {
let (manager, site, cmds_seen) = setup();
let seed = vec![
serde_json::json!({"name": "seed_a", "value": "s1", "domain": ".rec.test"}),
serde_json::json!({"name": "seed_b", "value": "s2", "domain": ".rec.test"}),
];
let err = manager
.refresh_with_seed(&seed)
.await
.expect_err("worker never replies, so set_cookies must error");
match &err {
SiteError::RefreshFailed { site: id, reason } => {
assert_eq!(id, &"rec");
assert!(
reason.starts_with("set_cookies:"),
"expected `set_cookies:` error prefix, got: {reason}"
);
}
other => panic!("expected RefreshFailed, got {other:?}"),
}
assert_eq!(cmds_seen.load(Ordering::SeqCst), 1);
assert_eq!(site.refresh_count(), 0);
assert!(manager.cookies().await.is_empty());
}