hackerone-api 0.2.0

Unofficial, dependency-light Rust client for the HackerOne API (v1): submit reports, read your reports, hacktivity, balance, and earnings.
Documentation
//! Live end-to-end tests against the real HackerOne API.
//!
//! Ignored by default so `cargo test` never touches the network. Run with
//! credentials:
//!
//! ```sh
//! HACKERONE_API_IDENTIFIER=smurf77 \
//! HACKERONE_API_TOKEN=... \
//!   cargo test --test live -- --ignored --nocapture
//! ```
//!
//! These cover the **hacker** surface (`/v1/hackers/*`), which is what a
//! researcher's API token can reach. The customer endpoints (`/v1/me`,
//! `/v1/programs/*`) return 401 for a hacker-only token and are intentionally
//! not exercised here.

use hackerone_api::{Client, HacktivityQuery, PageQuery};

/// Build a client from the environment, or skip the test when unset.
fn live_client() -> Option<Client> {
    let identifier = std::env::var("HACKERONE_API_IDENTIFIER").ok()?;
    let token = std::env::var("HACKERONE_API_TOKEN").ok()?;
    if identifier.is_empty() || token.is_empty() {
        return None;
    }
    Some(Client::new(identifier, token))
}

#[test]
#[ignore = "hits the live HackerOne API; run with --ignored and credentials"]
fn my_reports_round_trips() {
    let Some(client) = live_client() else {
        eprintln!("skipping: HACKERONE_API_IDENTIFIER / HACKERONE_API_TOKEN not set");
        return;
    };

    let page = client
        .my_reports(&PageQuery::new().page(1, 25))
        .expect("my_reports() against the live API");

    eprintln!("{} report(s) on page 1", page.len());
    for (id, report) in page.ids().zip(page.items()) {
        eprintln!(
            "  {}  {:<12} {:?}",
            id.unwrap_or("?"),
            report.state.as_deref().unwrap_or("?"),
            report.title.as_deref().unwrap_or("(untitled)")
        );
    }
}

#[test]
#[ignore = "hits the live HackerOne API; run with --ignored and credentials"]
fn my_report_fetches_a_specific_report() {
    let Some(client) = live_client() else {
        eprintln!("skipping: credentials not set");
        return;
    };

    // Prefer the id from the environment; fall back to the first own report.
    let id = match std::env::var("H1_REPORT_ID") {
        Ok(id) if !id.is_empty() => id,
        _ => {
            let page = client
                .my_reports(&PageQuery::new().page(1, 1))
                .expect("my_reports()");
            let first = page.ids().next().flatten().map(str::to_string);
            match first {
                Some(id) => id,
                None => {
                    eprintln!("no reports on the account — nothing to fetch");
                    return;
                }
            }
        }
    };

    let report = client
        .my_report(&id)
        .unwrap_or_else(|e| panic!("my_report({id}) failed: {e}"));
    eprintln!(
        "report {}: state={:?} title={:?}",
        id,
        report.state.as_deref().unwrap_or("?"),
        report.title.as_deref().unwrap_or("(untitled)")
    );
    assert!(
        report.title.as_deref().is_some_and(|t| !t.is_empty()),
        "report {id} had no title: {report:?}"
    );
}

#[test]
#[ignore = "hits the live HackerOne API; run with --ignored and credentials"]
fn balance_round_trips() {
    let Some(client) = live_client() else {
        eprintln!("skipping: credentials not set");
        return;
    };

    let balance = client.balance().expect("balance() against the live API");
    eprintln!("balance = {:?}", balance.balance);
    assert!(balance.balance.is_some(), "no balance returned");
}

#[test]
#[ignore = "hits the live HackerOne API; run with --ignored and credentials"]
fn earnings_round_trips() {
    let Some(client) = live_client() else {
        eprintln!("skipping: credentials not set");
        return;
    };

    let page = client
        .earnings(&PageQuery::new().page(1, 25))
        .expect("earnings() against the live API");
    eprintln!("{} earning(s)", page.len());
}

#[test]
#[ignore = "hits the live HackerOne API; run with --ignored and credentials"]
fn hacktivity_is_publicly_readable() {
    // The public feed also works without credentials.
    let client = live_client().unwrap_or_else(Client::anonymous);

    let page = client
        .hacktivity(&HacktivityQuery::new().page(1, 3))
        .expect("hacktivity() against the live API");

    eprintln!("{} hacktivity item(s)", page.len());
    assert!(!page.is_empty(), "expected at least one hacktivity item");
}