use hypixel::util::market;
use hypixel::{Error, HypixelClient};
use wiremock::matchers::{header, method, path, query_param};
use wiremock::{Mock, MockServer, ResponseTemplate};
const STATUS: &str = include_str!("fixtures/status.json");
const BAZAAR: &str = include_str!("fixtures/bazaar.json");
const AUCTIONS: &str = include_str!("fixtures/auctions.json");
async fn keyed(server: &MockServer) -> HypixelClient {
HypixelClient::builder()
.api_key("test-key")
.base_url(server.uri())
.build()
}
#[tokio::test]
async fn status_parses_session_and_rate_limit() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v2/status"))
.and(query_param("uuid", "abc"))
.and(header("API-Key", "test-key"))
.respond_with(
ResponseTemplate::new(200)
.insert_header("RateLimit-Limit", "120")
.insert_header("RateLimit-Remaining", "119")
.insert_header("RateLimit-Reset", "42")
.set_body_string(STATUS),
)
.mount(&server)
.await;
let client = keyed(&server).await;
let session = client.status("abc").await.unwrap();
assert!(session.online);
assert_eq!(session.game_type.as_deref(), Some("SKYBLOCK"));
let rl = client.rate_limit();
assert_eq!(rl.limit, Some(120));
assert_eq!(rl.remaining, Some(119));
assert_eq!(rl.reset, Some(42));
}
#[tokio::test]
async fn bazaar_is_keyless_and_spread_computes() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v2/skyblock/bazaar"))
.respond_with(ResponseTemplate::new(200).set_body_string(BAZAAR))
.mount(&server)
.await;
let client = HypixelClient::builder().base_url(server.uri()).build();
let bazaar = client.skyblock_bazaar().await.unwrap();
let spreads = market::bazaar_spreads(&bazaar);
let spread = spreads.get("INK_SACK:3").unwrap();
assert_eq!(spread.sell_order_price, 4.2);
assert_eq!(spread.buy_order_price, 4.8);
}
#[tokio::test]
async fn auctions_lowest_bin_aggregates() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v2/skyblock/auctions"))
.respond_with(ResponseTemplate::new(200).set_body_string(AUCTIONS))
.mount(&server)
.await;
let client = HypixelClient::builder().base_url(server.uri()).build();
let page = client.skyblock_auctions(0).await.unwrap();
assert_eq!(page.total_auctions, 2);
let lowest = market::lowest_bin(&page.auctions);
assert_eq!(lowest.get("Hyperion"), Some(&850_000_000));
}
#[tokio::test]
async fn invalid_key_maps_to_error() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v2/player"))
.respond_with(ResponseTemplate::new(403).set_body_string(r#"{"success":false}"#))
.mount(&server)
.await;
let client = keyed(&server).await;
let err = client.player("abc").await.unwrap_err();
assert!(matches!(err, Error::InvalidApiKey));
}
#[tokio::test]
async fn rate_limited_maps_to_error_with_retry_after() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v2/player"))
.respond_with(ResponseTemplate::new(429).insert_header("Retry-After", "30"))
.mount(&server)
.await;
let client = keyed(&server).await;
let err = client.player("abc").await.unwrap_err();
match err {
Error::RateLimited { retry_after } => {
assert_eq!(retry_after.map(|d| d.as_secs()), Some(30));
}
other => panic!("expected RateLimited, got {other:?}"),
}
}
#[tokio::test]
async fn keyed_endpoint_without_key_fails_fast() {
let client = HypixelClient::unauthenticated();
let err = client.player("abc").await.unwrap_err();
assert!(matches!(err, Error::MissingApiKey));
}