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));
}
#[tokio::test]
async fn auctions_all_walks_every_page() {
let server = MockServer::start().await;
for page in 0..3 {
Mock::given(method("GET"))
.and(path("/v2/skyblock/auctions"))
.and(query_param("page", page.to_string()))
.respond_with(ResponseTemplate::new(200).set_body_string(format!(
r#"{{"success":true,"page":{page},"totalPages":3,"totalAuctions":3,
"auctions":[{{"uuid":"a{page}","item_name":"Item {page}","bin":true,"starting_bid":{}}}]}}"#,
(page + 1) * 100
)))
.expect(1)
.mount(&server)
.await;
}
let client = HypixelClient::builder().base_url(server.uri()).build();
let auctions = client.skyblock_auctions_all().await.unwrap();
assert_eq!(auctions.len(), 3);
let mut names: Vec<_> = auctions.iter().map(|a| a.item_name.as_str()).collect();
names.sort();
assert_eq!(names, vec!["Item 0", "Item 1", "Item 2"]);
}
#[tokio::test]
async fn rate_limited_request_retries_when_configured() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v2/player"))
.respond_with(ResponseTemplate::new(429).insert_header("Retry-After", "0"))
.up_to_n_times(1)
.expect(1)
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/v2/player"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(r#"{"success":true,"player":{"uuid":"abc"}}"#),
)
.expect(1)
.mount(&server)
.await;
let client = HypixelClient::builder()
.api_key("test-key")
.base_url(server.uri())
.retry_on_rate_limit(2)
.build();
let player = client.player("abc").await.unwrap();
assert!(player.is_some());
}
#[tokio::test]
async fn cached_responses_skip_the_network() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v2/skyblock/bazaar"))
.respond_with(ResponseTemplate::new(200).set_body_string(BAZAAR))
.expect(1)
.mount(&server)
.await;
let client = HypixelClient::builder()
.base_url(server.uri())
.cache_ttl(std::time::Duration::from_secs(60))
.build();
let first = client.skyblock_bazaar().await.unwrap();
let second = client.skyblock_bazaar().await.unwrap();
assert_eq!(first.products.len(), second.products.len());
}