browserbase 0.1.0

Rust SDK for Browserbase - cloud browser automation platform
Documentation
use browserbase::{
    Browserbase,
    models::{CreateSessionRequest, ListSessionsParams, UpdateSessionRequest},
};
use serde_json::json;
use wiremock::{
    Mock, MockServer, ResponseTemplate,
    matchers::{body_json, header, method, path, query_param},
};

#[tokio::test]
async fn create_session_sends_api_key_and_parses_response() {
    let server = MockServer::start().await;
    let base_url = format!("{}/v1/", server.uri());

    Mock::given(method("POST"))
        .and(path("/v1/sessions"))
        .and(header("X-BB-API-Key", "test-key"))
        .and(body_json(json!({"projectId":"proj_123"})))
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
            "id": "sess_1",
            "projectId": "proj_123",
            "status": "RUNNING",
            "connectUrl": "wss://example/connect"
        })))
        .mount(&server)
        .await;

    let bb = Browserbase::builder("test-key")
        .unwrap()
        .base_url(&base_url)
        .unwrap()
        .build()
        .unwrap();

    let session = bb
        .sessions()
        .create(CreateSessionRequest::new("proj_123"))
        .await
        .unwrap();

    assert_eq!(session.id, "sess_1");
    assert_eq!(session.project_id.as_deref(), Some("proj_123"));
    assert_eq!(session.status.as_deref(), Some("RUNNING"));
    assert_eq!(
        session.connect_url.as_deref(),
        Some("wss://example/connect")
    );
}

#[tokio::test]
async fn list_sessions_supports_query_params() {
    let server = MockServer::start().await;
    let base_url = format!("{}/v1/", server.uri());

    Mock::given(method("GET"))
        .and(path("/v1/sessions"))
        .and(header("X-BB-API-Key", "test-key"))
        .and(query_param("status", "RUNNING"))
        .respond_with(ResponseTemplate::new(200).set_body_json(json!([
            {"id": "sess_1"},
            {"id": "sess_2"}
        ])))
        .mount(&server)
        .await;

    let bb = Browserbase::builder("test-key")
        .unwrap()
        .base_url(&base_url)
        .unwrap()
        .build()
        .unwrap();

    let sessions = bb
        .sessions()
        .list(ListSessionsParams::new().status("RUNNING"))
        .await
        .unwrap();

    assert_eq!(sessions.len(), 2);
    assert_eq!(sessions[0].id, "sess_1");
    assert_eq!(sessions[1].id, "sess_2");
}

#[tokio::test]
async fn update_session_posts_to_session_id() {
    let server = MockServer::start().await;
    let base_url = format!("{}/v1/", server.uri());

    Mock::given(method("POST"))
        .and(path("/v1/sessions/sess_1"))
        .and(header("X-BB-API-Key", "test-key"))
        .and(body_json(json!({
            "projectId": "proj_123",
            "status": "REQUEST_RELEASE"
        })))
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
            "id": "sess_1",
            "status": "REQUEST_RELEASE"
        })))
        .mount(&server)
        .await;

    let bb = Browserbase::builder("test-key")
        .unwrap()
        .base_url(&base_url)
        .unwrap()
        .build()
        .unwrap();

    let session = bb
        .sessions()
        .update(
            "sess_1",
            UpdateSessionRequest::new("proj_123", "REQUEST_RELEASE"),
        )
        .await
        .unwrap();

    assert_eq!(session.id, "sess_1");
    assert_eq!(session.status.as_deref(), Some("REQUEST_RELEASE"));
}

#[tokio::test]
async fn downloads_zip_returns_raw_bytes() {
    let server = MockServer::start().await;
    let base_url = format!("{}/v1/", server.uri());

    Mock::given(method("GET"))
        .and(path("/v1/sessions/sess_1/downloads"))
        .and(header("X-BB-API-Key", "test-key"))
        .respond_with(ResponseTemplate::new(200).set_body_bytes(vec![1u8, 2, 3]))
        .mount(&server)
        .await;

    let bb = Browserbase::builder("test-key")
        .unwrap()
        .base_url(&base_url)
        .unwrap()
        .build()
        .unwrap();

    let bytes = bb.sessions().downloads_zip("sess_1").await.unwrap();
    assert_eq!(&bytes[..], &[1u8, 2, 3]);
}

#[tokio::test]
async fn get_session_returns_session_details() {
    let server = MockServer::start().await;
    let base_url = format!("{}/v1/", server.uri());

    Mock::given(method("GET"))
        .and(path("/v1/sessions/sess_1"))
        .and(header("X-BB-API-Key", "test-key"))
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
            "id": "sess_1",
            "projectId": "proj_123",
            "status": "RUNNING",
            "connectUrl": "wss://example/connect",
            "region": "us-west-2"
        })))
        .mount(&server)
        .await;

    let bb = Browserbase::builder("test-key")
        .unwrap()
        .base_url(&base_url)
        .unwrap()
        .build()
        .unwrap();

    let session = bb.sessions().get("sess_1").await.unwrap();
    assert_eq!(session.id, "sess_1");
    assert_eq!(session.region.as_deref(), Some("us-west-2"));
}

#[tokio::test]
async fn delete_session_returns_unit() {
    let server = MockServer::start().await;
    let base_url = format!("{}/v1/", server.uri());

    Mock::given(method("DELETE"))
        .and(path("/v1/sessions/sess_1"))
        .and(header("X-BB-API-Key", "test-key"))
        .respond_with(ResponseTemplate::new(204))
        .mount(&server)
        .await;

    let bb = Browserbase::builder("test-key")
        .unwrap()
        .base_url(&base_url)
        .unwrap()
        .build()
        .unwrap();

    bb.sessions().delete("sess_1").await.unwrap();
}

#[tokio::test]
async fn create_session_with_optional_fields_sends_all_params() {
    let server = MockServer::start().await;
    let base_url = format!("{}/v1/", server.uri());

    Mock::given(method("POST"))
        .and(path("/v1/sessions"))
        .and(header("X-BB-API-Key", "test-key"))
        .and(body_json(json!({
            "projectId": "proj_123",
            "region": "eu-central-1",
            "keepAlive": true,
            "contextId": "ctx_456",
            "userMetadata": {"tag": "test"}
        })))
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
            "id": "sess_1",
            "projectId": "proj_123",
            "status": "RUNNING"
        })))
        .mount(&server)
        .await;

    let bb = Browserbase::builder("test-key")
        .unwrap()
        .base_url(&base_url)
        .unwrap()
        .build()
        .unwrap();

    let req = CreateSessionRequest::new("proj_123")
        .region("eu-central-1")
        .keep_alive(true)
        .context_id("ctx_456")
        .user_metadata(json!({"tag": "test"}));

    let session = bb.sessions().create(req).await.unwrap();
    assert_eq!(session.id, "sess_1");
}

#[tokio::test]
async fn session_logs_returns_json() {
    let server = MockServer::start().await;
    let base_url = format!("{}/v1/", server.uri());

    Mock::given(method("GET"))
        .and(path("/v1/sessions/sess_1/logs"))
        .and(header("X-BB-API-Key", "test-key"))
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
            "logs": [],
            "total": 0
        })))
        .mount(&server)
        .await;

    let bb = Browserbase::builder("test-key")
        .unwrap()
        .base_url(&base_url)
        .unwrap()
        .build()
        .unwrap();

    let logs = bb.sessions().logs("sess_1").await.unwrap();
    assert!(logs.is_object());
}

#[tokio::test]
async fn create_upload_multipart_file() {
    use wiremock::matchers::header_exists;

    let server = MockServer::start().await;
    let base_url = format!("{}/v1/", server.uri());

    Mock::given(method("POST"))
        .and(path("/v1/sessions/sess_1/uploads"))
        .and(header_exists("X-BB-API-Key"))
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({
            "success": true,
            "filename": "upload.bin"
        })))
        .mount(&server)
        .await;

    let bb = Browserbase::builder("test-key")
        .unwrap()
        .base_url(&base_url)
        .unwrap()
        .build()
        .unwrap();

    let result = bb
        .sessions()
        .create_upload_from_bytes("sess_1", "upload.bin", bytes::Bytes::from(vec![1, 2, 3]))
        .await
        .unwrap();

    assert!(result.is_object());
}

#[tokio::test]
async fn api_error_on_429_status() {
    let server = MockServer::start().await;
    let base_url = format!("{}/v1/", server.uri());

    Mock::given(method("POST"))
        .and(path("/v1/sessions"))
        .and(header("X-BB-API-Key", "test-key"))
        .respond_with(
            ResponseTemplate::new(429)
                .set_body_json(json!({"error": "Too Many Requests", "message": "Limit reached"})),
        )
        .mount(&server)
        .await;

    let bb = Browserbase::builder("test-key")
        .unwrap()
        .base_url(&base_url)
        .unwrap()
        .build()
        .unwrap();

    let result = bb
        .sessions()
        .create(CreateSessionRequest::new("proj_123"))
        .await;
    assert!(result.is_err());

    let err = result.unwrap_err();
    match err {
        browserbase::error::Error::Api { status, body } => {
            assert_eq!(status.as_u16(), 429);
            assert!(body.contains("Too Many Requests"));
        }
        _ => panic!("Expected API error"),
    }
}