use std::net::SocketAddr;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
use captchaforge::browser::{launch_firefox, FoxBrowserConfig, Page};
use std::sync::LazyLock;
use tokio::sync::Semaphore;
static BROWSER_SEM: LazyLock<Semaphore> = LazyLock::new(|| Semaphore::new(4));
pub struct TestBrowser {
page: Page,
}
impl std::ops::Deref for TestBrowser {
type Target = Page;
fn deref(&self) -> &Self::Target {
&self.page
}
}
impl TestBrowser {
pub async fn new_page(&self, url: &str) -> anyhow::Result<&Page> {
self.page.goto(url).await?;
Ok(&self.page)
}
pub async fn close(&self) -> anyhow::Result<()> {
self.page.close().await
}
}
pub async fn launch_browser() -> TestBrowser {
use std::sync::atomic::{AtomicUsize, Ordering};
static COUNTER: AtomicUsize = AtomicUsize::new(0);
let _permit = BROWSER_SEM.acquire().await.expect("semaphore never closed");
let firefox_path = which::which("firefox")
.or_else(|_| which::which("/snap/bin/firefox"))
.unwrap_or_else(|_| std::path::PathBuf::from("firefox"));
let user_data_dir = std::env::temp_dir().join(format!(
"captchaforge-test-{}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos(),
COUNTER.fetch_add(1, Ordering::SeqCst)
));
let config = FoxBrowserConfig {
executable_path: Some(firefox_path.to_string_lossy().to_string()),
profile_dir: Some(user_data_dir.to_string_lossy().to_string()),
headless: true,
viewport_width: 1280,
viewport_height: 720,
user_agent: None,
..Default::default()
};
let page = launch_firefox(config)
.await
.expect("failed to launch Firefox");
TestBrowser { page }
}
#[allow(dead_code)]
pub async fn serve_once(html: String) -> SocketAddr {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let html_clone = html.clone();
tokio::spawn(async move {
let mut served = 0;
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(30);
while served < 2 && tokio::time::Instant::now() < deadline {
let timeout =
tokio::time::timeout(std::time::Duration::from_millis(500), listener.accept());
match timeout.await {
Ok(Ok((mut stream, _))) => {
let mut buf = [0u8; 1024];
let _ = stream.read(&mut buf).await;
let req = String::from_utf8_lossy(&buf);
let response = if req.starts_with("GET / ") || req.starts_with("GET / HTTP") {
served += 1;
format!(
"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
html_clone.len(),
html_clone
)
} else {
"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
.to_string()
};
let _ = stream.write_all(response.as_bytes()).await;
}
_ => continue,
}
}
});
addr
}