flarer 0.1.0

Rust client and CLI for Cloudflare's Browser Rendering REST API (content, screenshot, PDF, snapshot, markdown, scrape, JSON extraction, links, crawl).
Documentation
//! Integration tests against a `wiremock` Cloudflare stand-in.

use flarer::{Account, Flarer, JsonOptions, Output, ResponseFormat, Tool};
use reqwest::Url;
use serde_json::json;
use wiremock::matchers::{header, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};

const ACCOUNT: &str = "test-account";

async fn make_flarer(server: &MockServer) -> Flarer {
    let base = Url::parse(&server.uri()).unwrap();
    Flarer::builder()
        .account(Account::new(ACCOUNT, "test-token"))
        .base_url(base)
        .build()
        .unwrap()
}

#[tokio::test]
async fn verify_succeeds_on_2xx() {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path(format!("/accounts/{ACCOUNT}/tokens/verify")))
        .and(header("authorization", "Bearer test-token"))
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({"success": true})))
        .mount(&server)
        .await;

    let flarer = make_flarer(&server).await;
    flarer.verify().await.expect("verify should succeed");
}

#[tokio::test]
async fn verify_fails_on_401() {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path(format!("/accounts/{ACCOUNT}/tokens/verify")))
        .respond_with(ResponseTemplate::new(401).set_body_string("nope"))
        .mount(&server)
        .await;

    let flarer = make_flarer(&server).await;
    let err = flarer.verify().await.unwrap_err();
    assert!(matches!(err, flarer::FlarerError::Auth(_)));
}

#[tokio::test]
async fn markdown_returns_json() {
    let server = MockServer::start().await;
    let body = json!({"success": true, "result": "# hello"});
    Mock::given(method("POST"))
        .and(path(format!(
            "/accounts/{ACCOUNT}/browser-rendering/markdown"
        )))
        .respond_with(ResponseTemplate::new(200).set_body_json(body.clone()))
        .mount(&server)
        .await;

    let flarer = make_flarer(&server).await;
    let out = flarer
        .run(Tool::Markdown, "https://example.com")
        .await
        .unwrap();
    match out {
        Output::Json(v) => assert_eq!(v, body),
        other => panic!("unexpected: {other:?}"),
    }
}

#[tokio::test]
async fn screenshot_writes_file() {
    let server = MockServer::start().await;
    let png = vec![0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
    Mock::given(method("POST"))
        .and(path(format!(
            "/accounts/{ACCOUNT}/browser-rendering/screenshot"
        )))
        .respond_with(
            ResponseTemplate::new(200)
                .insert_header("content-type", "image/png")
                .set_body_bytes(png.clone()),
        )
        .mount(&server)
        .await;

    let tmp = tempdir();
    let flarer = Flarer::builder()
        .account(Account::new(ACCOUNT, "test-token"))
        .base_url(Url::parse(&server.uri()).unwrap())
        .output_dir(&tmp)
        .build()
        .unwrap();

    let out = flarer
        .run(Tool::Screenshot, "https://example.com")
        .await
        .unwrap();
    let path = match out {
        Output::File(p) => p,
        other => panic!("unexpected: {other:?}"),
    };
    let bytes = std::fs::read(&path).unwrap();
    assert_eq!(bytes, png);
    let _ = std::fs::remove_dir_all(&tmp);
}

#[tokio::test]
async fn json_tool_serializes_response_format_with_type_key() {
    let server = MockServer::start().await;
    Mock::given(method("POST"))
        .and(path(format!("/accounts/{ACCOUNT}/browser-rendering/json")))
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({"success": true})))
        .mount(&server)
        .await;

    let flarer = make_flarer(&server).await;
    let opts = JsonOptions::new()
        .with_prompt("hi")
        .with_response_format(ResponseFormat::json_schema(json!({"type": "object"})));
    let _ = flarer.json("https://example.com", opts).await.unwrap();

    // Inspect the actually-received body.
    let received = server.received_requests().await.unwrap();
    let last = received.last().unwrap();
    let body: serde_json::Value = serde_json::from_slice(&last.body).unwrap();
    assert_eq!(body["url"], "https://example.com/");
    assert_eq!(body["prompt"], "hi");
    assert_eq!(body["response_format"]["type"], "json_schema");
    assert_eq!(body["response_format"]["schema"]["type"], "object");
}

#[tokio::test]
async fn api_error_propagates_status_and_body() {
    let server = MockServer::start().await;
    Mock::given(method("POST"))
        .and(path(format!(
            "/accounts/{ACCOUNT}/browser-rendering/content"
        )))
        .respond_with(ResponseTemplate::new(500).set_body_string("boom"))
        .mount(&server)
        .await;

    let flarer = make_flarer(&server).await;
    let err = flarer
        .text(Tool::Content, "https://example.com")
        .await
        .unwrap_err();
    match err {
        flarer::FlarerError::Api { status, body } => {
            assert_eq!(status, 500);
            assert_eq!(body, "boom");
        }
        other => panic!("unexpected error: {other:?}"),
    }
}

fn tempdir() -> std::path::PathBuf {
    let mut p = std::env::temp_dir();
    p.push(format!("flarer-test-{}", std::process::id()));
    let _ = std::fs::create_dir_all(&p);
    p
}