use std::time::Duration;
use anakin::{Client, Error};
use mockito::Matcher;
use serde_json::json;
fn build_client(server_url: &str) -> Client {
Client::builder()
.api_key("ak-test")
.base_url(server_url)
.max_retries(2)
.poll_interval(Duration::from_millis(5))
.poll_max_interval(Duration::from_millis(20))
.poll_timeout(Duration::from_secs(3))
.timeout(Duration::from_secs(5))
.build()
.expect("build client")
}
#[tokio::test]
async fn requires_api_key() {
std::env::remove_var("ANAKIN_API_KEY");
let err = Client::builder().build().unwrap_err();
let msg = err.to_string();
assert!(msg.to_lowercase().contains("api key"), "unexpected: {msg}");
}
#[tokio::test]
async fn version_is_exposed() {
assert_eq!(anakin::VERSION, "0.1.0");
}
#[tokio::test]
async fn countries_bundled() {
let all = anakin::supported_countries();
assert!(!all.is_empty());
assert!(all.iter().any(|(c, _)| *c == "US"));
assert!(all.len() >= 50);
}
#[tokio::test]
async fn scrape_polls_to_completion() {
let mut server = mockito::Server::new_async().await;
let _m1 = server
.mock("POST", "/url-scraper")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(json!({"job_id": "j1"}).to_string())
.create_async()
.await;
let _m2 = server
.mock("GET", "/url-scraper/j1")
.expect_at_least(2)
.with_status(200)
.with_header("content-type", "application/json")
.with_body_from_request(|_req| {
br##"{"status":"completed","result":{"url":"https://example.com","markdown":"# Hi","status_code":200}}"##.to_vec()
})
.create_async()
.await;
let client = build_client(&server.url());
let doc = client.scrape("https://example.com").await.expect("scrape ok");
assert_eq!(doc.url.as_deref(), Some("https://example.com"));
assert_eq!(doc.markdown.as_deref(), Some("# Hi"));
assert_eq!(doc.status_code, Some(200));
}
#[tokio::test]
async fn scrape_running_then_completed() {
let mut server = mockito::Server::new_async().await;
let _post = server
.mock("POST", "/url-scraper")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(json!({"job_id": "jp"}).to_string())
.create_async()
.await;
let _running = server
.mock("GET", "/url-scraper/jp")
.with_status(200)
.with_body(r#"{"status":"running"}"#)
.expect(1)
.create_async()
.await;
let _completed = server
.mock("GET", "/url-scraper/jp")
.with_status(200)
.with_body(r#"{"status":"completed","result":{"markdown":"ok"}}"#)
.create_async()
.await;
let client = build_client(&server.url());
let doc = client.scrape("https://example.com").await.expect("scrape ok");
assert_eq!(doc.markdown.as_deref(), Some("ok"));
}
#[tokio::test]
async fn scrape_failed_job() {
let mut server = mockito::Server::new_async().await;
let _m1 = server
.mock("POST", "/url-scraper")
.with_status(200)
.with_body(json!({"job_id": "jbad"}).to_string())
.create_async()
.await;
let _m2 = server
.mock("GET", "/url-scraper/jbad")
.with_status(200)
.with_body(r#"{"status":"failed","error":"target unreachable","job_id":"jbad"}"#)
.create_async()
.await;
let client = build_client(&server.url());
let err = client.scrape("https://example.com").await.unwrap_err();
match err {
Error::JobFailed { reason, job_id } => {
assert_eq!(reason, "target unreachable");
assert_eq!(job_id.as_deref(), Some("jbad"));
}
other => panic!("expected JobFailed, got {other:?}"),
}
}
#[tokio::test]
async fn scrape_polling_timeout() {
let mut server = mockito::Server::new_async().await;
let _post = server
.mock("POST", "/url-scraper")
.with_status(200)
.with_body(json!({"job_id": "jslow"}).to_string())
.create_async()
.await;
let _poll = server
.mock("GET", "/url-scraper/jslow")
.with_status(200)
.with_body(r#"{"status":"running","job_id":"jslow"}"#)
.expect_at_least(1)
.create_async()
.await;
let client = Client::builder()
.api_key("ak-test")
.base_url(&server.url())
.poll_interval(Duration::from_millis(20))
.poll_max_interval(Duration::from_millis(20))
.poll_timeout(Duration::from_millis(80))
.build()
.unwrap();
let err = client.scrape("https://example.com").await.unwrap_err();
assert!(matches!(err, Error::JobTimeout { .. }), "got {err:?}");
}
#[tokio::test]
async fn map_polls() {
let mut server = mockito::Server::new_async().await;
let _m1 = server
.mock("POST", "/map")
.with_status(200)
.with_body(json!({"job_id": "m1"}).to_string())
.create_async()
.await;
let _m2 = server
.mock("GET", "/map/m1")
.with_status(200)
.with_body(r#"{"status":"completed","result":{"url":"https://x.com","links":["https://x.com/a","https://x.com/b"],"total":2}}"#)
.create_async()
.await;
let client = build_client(&server.url());
let r = client.map("https://x.com").await.unwrap();
assert_eq!(r.total, Some(2));
assert_eq!(r.links.len(), 2);
}
#[tokio::test]
async fn crawl_polls() {
let mut server = mockito::Server::new_async().await;
let _m1 = server
.mock("POST", "/crawl")
.with_status(200)
.with_body(json!({"job_id": "c1"}).to_string())
.create_async()
.await;
let _m2 = server
.mock("GET", "/crawl/c1")
.with_status(200)
.with_body(r#"{"status":"completed","result":{"url":"https://x.com","pages":[{"url":"https://x.com/p","markdown":"hi"}],"total":1}}"#)
.create_async()
.await;
let client = build_client(&server.url());
let r = client.crawl("https://x.com").await.unwrap();
assert_eq!(r.total, Some(1));
assert_eq!(r.pages.len(), 1);
assert_eq!(r.pages[0].markdown.as_deref(), Some("hi"));
}
#[tokio::test]
async fn agentic_search_polls() {
let mut server = mockito::Server::new_async().await;
let _m1 = server
.mock("POST", "/agentic-search")
.with_status(200)
.with_body(json!({"job_id": "a1"}).to_string())
.create_async()
.await;
let _m2 = server
.mock("GET", "/agentic-search/a1")
.with_status(200)
.with_body(r#"{"status":"completed","result":{"answer":"42","sources":[{"url":"https://wiki.com","title":"life"}]}}"#)
.create_async()
.await;
let client = build_client(&server.url());
let r = client.agentic_search("meaning of life").await.unwrap();
assert_eq!(r.answer.as_deref(), Some("42"));
assert_eq!(r.sources.len(), 1);
}
#[tokio::test]
async fn wire_polls() {
let mut server = mockito::Server::new_async().await;
let _m1 = server
.mock("POST", "/holocron/task")
.with_status(200)
.with_body(json!({"job_id": "w1"}).to_string())
.create_async()
.await;
let _m2 = server
.mock("GET", "/holocron/task/w1")
.with_status(200)
.with_body(r#"{"status":"completed","result":{"action_id":"act-1","output":{"ok":true}}}"#)
.create_async()
.await;
let client = build_client(&server.url());
let r = client.wire("act-1", Some(json!({"query":"test"}))).await.unwrap();
assert_eq!(r.action_id.as_deref(), Some("act-1"));
}
#[tokio::test]
async fn search_sync() {
let mut server = mockito::Server::new_async().await;
let _m = server
.mock("POST", "/search")
.match_body(Matcher::PartialJson(json!({"prompt": "rust reqwest"})))
.with_status(200)
.with_body(r#"{"query":"rust reqwest","results":[{"title":"docs.rs","url":"https://docs.rs","snippet":"..."}]}"#)
.create_async()
.await;
let client = build_client(&server.url());
let r = client.search("rust reqwest").await.unwrap();
assert_eq!(r.query.as_deref(), Some("rust reqwest"));
assert_eq!(r.results.len(), 1);
}
#[tokio::test]
async fn unauthorized() {
let mut server = mockito::Server::new_async().await;
let _m = server
.mock("POST", "/url-scraper")
.with_status(401)
.with_body(r#"{"error":"bad key","code":"unauthorized"}"#)
.create_async()
.await;
let client = build_client(&server.url());
let err = client.scrape("https://example.com").await.unwrap_err();
assert!(matches!(err, Error::Authentication { status: 401, .. }), "got {err:?}");
}
#[tokio::test]
async fn payment_required() {
let mut server = mockito::Server::new_async().await;
let _m = server
.mock("POST", "/url-scraper")
.with_status(402)
.with_body(r#"{"error":"out of credits","code":"insufficient_credits","balance":3,"required":10}"#)
.create_async()
.await;
let client = build_client(&server.url());
let err = client.scrape("https://example.com").await.unwrap_err();
match err {
Error::InsufficientCredits { balance, required, .. } => {
assert_eq!(balance, 3);
assert_eq!(required, 10);
}
other => panic!("expected InsufficientCredits, got {other:?}"),
}
}
#[tokio::test]
async fn bad_request() {
let mut server = mockito::Server::new_async().await;
let _m = server
.mock("POST", "/url-scraper")
.with_status(400)
.with_body(r#"{"error":"missing url","code":"validation_error"}"#)
.create_async()
.await;
let client = build_client(&server.url());
let err = client.scrape("").await.unwrap_err();
assert!(matches!(err, Error::InvalidRequest { status: 400, .. }), "got {err:?}");
}
#[tokio::test]
async fn rate_limited_after_retries() {
let mut server = mockito::Server::new_async().await;
let _m = server
.mock("POST", "/url-scraper")
.expect_at_least(2)
.with_status(429)
.with_header("retry-after", "0")
.with_body(r#"{"error":"slow down"}"#)
.create_async()
.await;
let client = build_client(&server.url());
let err = client.scrape("https://example.com").await.unwrap_err();
assert!(matches!(err, Error::RateLimit { status: 429, .. }), "got {err:?}");
}
#[tokio::test]
async fn server_error() {
let mut server = mockito::Server::new_async().await;
let _m = server
.mock("POST", "/url-scraper")
.expect_at_least(1)
.with_status(503)
.with_body(r#"{"error":"down"}"#)
.create_async()
.await;
let client = build_client(&server.url());
let err = client.scrape("https://example.com").await.unwrap_err();
assert!(matches!(err, Error::Server { status: 503, .. }), "got {err:?}");
}
#[tokio::test]
async fn retries_5xx_then_succeeds() {
let mut server = mockito::Server::new_async().await;
let _bad = server
.mock("POST", "/url-scraper")
.with_status(502)
.with_body(r#"{"error":"bad gateway"}"#)
.expect(1)
.create_async()
.await;
let _ok = server
.mock("POST", "/url-scraper")
.with_status(200)
.with_body(json!({"job_id": "j2"}).to_string())
.create_async()
.await;
let _poll = server
.mock("GET", "/url-scraper/j2")
.with_status(200)
.with_body(r#"{"status":"completed","result":{"url":"https://example.com","markdown":"ok"}}"#)
.create_async()
.await;
let client = build_client(&server.url());
let doc = client.scrape("https://example.com").await.unwrap();
assert_eq!(doc.markdown.as_deref(), Some("ok"));
}
#[tokio::test]
async fn sessions_list() {
let mut server = mockito::Server::new_async().await;
let _m = server
.mock("GET", "/browser-sessions")
.with_status(200)
.with_body(r#"{"sessions":[{"id":"s1","name":"login"}]}"#)
.create_async()
.await;
let client = build_client(&server.url());
let list = client.sessions().list().await.unwrap();
assert_eq!(list.len(), 1);
assert_eq!(list[0].name.as_deref(), Some("login"));
}
#[tokio::test]
async fn sessions_create_and_delete() {
let mut server = mockito::Server::new_async().await;
let _create = server
.mock("POST", "/browser-sessions")
.with_status(200)
.with_body(r#"{"id":"s2","name":"new"}"#)
.create_async()
.await;
let _delete = server
.mock("DELETE", "/browser-sessions/s2")
.with_status(200)
.with_body("{}")
.create_async()
.await;
let client = build_client(&server.url());
let created = client.sessions().create("new", None).await.unwrap();
assert_eq!(created.id.as_deref(), Some("s2"));
client.sessions().delete("s2").await.unwrap();
}