use koios_sdk::{
types::{EpochNo, IncludeNextEpoch},
Client,
};
use pretty_assertions::assert_eq;
use serde_json::json;
use wiremock::{
matchers::{method, path, query_param},
Mock, MockServer, ResponseTemplate,
};
async fn setup_test_client() -> (MockServer, Client) {
let mock_server = MockServer::start().await;
let client = Client::builder()
.base_url(mock_server.uri())
.build()
.unwrap();
(mock_server, client)
}
#[tokio::test]
async fn test_get_epoch_info() {
let (mock_server, client) = setup_test_client().await;
let mock_response = json!([{
"epoch_no": 321,
"out_sum": "42000000000",
"fees": "1234567",
"tx_count": 8,
"blk_count": 21600,
"start_time": 1630106091,
"end_time": 1630451690,
"first_block_time": 1630106091,
"last_block_time": 1630451690,
"active_stake": "23456789000",
"total_rewards": "1234567890",
"avg_blk_reward": "57156"
}]);
Mock::given(method("GET"))
.and(path("/epoch_info"))
.respond_with(ResponseTemplate::new(200).set_body_json(&mock_response))
.mount(&mock_server)
.await;
let response = client.get_epoch_info(None, None).await.unwrap();
assert_eq!(response.len(), 1);
assert_eq!(response[0].epoch_no, 321);
assert_eq!(response[0].out_sum, "42000000000");
assert_eq!(response[0].fees, "1234567");
assert_eq!(response[0].tx_count, 8);
assert_eq!(response[0].blk_count, 21600);
assert_eq!(response[0].active_stake.as_ref().unwrap(), "23456789000");
assert_eq!(response[0].total_rewards.as_ref().unwrap(), "1234567890");
assert_eq!(response[0].avg_blk_reward.as_ref().unwrap(), "57156");
}
#[tokio::test]
async fn test_get_epoch_info_with_params() {
let (mock_server, client) = setup_test_client().await;
let epoch_no = EpochNo::new("321");
let include_next_epoch = IncludeNextEpoch::new(true);
Mock::given(method("GET"))
.and(path("/epoch_info"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!([{
"epoch_no": 321,
"out_sum": "42000000000",
"fees": "1234567",
"tx_count": 8,
"blk_count": 21600,
"start_time": 1630106091,
"end_time": 1630451690,
"first_block_time": 1630106091,
"last_block_time": 1630451690,
"active_stake": "23456789000",
"total_rewards": "1234567890",
"avg_blk_reward": "57156"
}])))
.expect(1)
.mount(&mock_server)
.await;
let response = client
.get_epoch_info(Some(epoch_no), Some(include_next_epoch))
.await
.unwrap();
assert_eq!(response.len(), 1);
assert_eq!(response[0].epoch_no, 321);
}
#[tokio::test]
async fn test_get_epoch_params() {
let (mock_server, client) = setup_test_client().await;
let mock_response = json!([{
"epoch_no": 321,
"min_fee_a": 44,
"min_fee_b": 155381,
"max_block_size": 65536,
"max_tx_size": 16384,
"max_bh_size": 1100,
"key_deposit": "2000000",
"pool_deposit": "500000000",
"max_epoch": 18,
"optimal_pool_count": 500,
"influence": 0.3,
"monetary_expand_rate": 0.003,
"treasury_growth_rate": 0.2,
"decentralisation": 0,
"extra_entropy": null,
"protocol_major": 6,
"protocol_minor": 0,
"min_utxo_value": "1000000",
"min_pool_cost": "340000000",
"nonce": "1a3be38bcbb7911969283716ad7aa550250226b76a61fc51cc9a9a35d9276d81",
"block_hash": "f144a8264acf4bdfe2e1241170969c930d64ab6b0996a4a45237b623f1dd670e",
"cost_models": null,
"price_mem": 0.0577,
"price_step": 0.0000721,
"max_tx_ex_mem": 10000000,
"max_tx_ex_steps": 10000000000_i64,
"max_block_ex_mem": 50000000,
"max_block_ex_steps": 40000000000_i64,
"max_val_size": 5000,
"collateral_percent": 150,
"max_collateral_inputs": 3,
"coins_per_utxo_size": "4310",
"coins_per_utxo_word": "34482"
}]);
Mock::given(method("GET"))
.and(path("/epoch_params"))
.respond_with(ResponseTemplate::new(200).set_body_json(&mock_response))
.mount(&mock_server)
.await;
let response = client.get_epoch_params(None).await.unwrap();
assert_eq!(response.len(), 1);
assert_eq!(response[0].epoch_no, 321);
assert_eq!(response[0].min_fee_a.unwrap(), 44);
assert_eq!(response[0].min_fee_b.unwrap(), 155381);
assert_eq!(response[0].max_block_size.unwrap(), 65536);
assert_eq!(response[0].max_tx_size.unwrap(), 16384);
assert_eq!(response[0].key_deposit.as_ref().unwrap(), "2000000");
assert_eq!(response[0].pool_deposit.as_ref().unwrap(), "500000000");
assert_eq!(response[0].max_epoch.unwrap(), 18);
assert_eq!(response[0].optimal_pool_count.unwrap(), 500);
assert_eq!(response[0].influence.unwrap(), 0.3);
}
#[tokio::test]
async fn test_get_epoch_params_with_epoch_no() {
let (mock_server, client) = setup_test_client().await;
let epoch_no = EpochNo::new("321");
let mock_response = json!([{
"epoch_no": 321,
"min_fee_a": 44,
"min_fee_b": 155381,
"max_block_size": 65536,
"max_tx_size": 16384,
"max_bh_size": 1100,
"key_deposit": "2000000",
"pool_deposit": "500000000",
"max_epoch": 18,
"optimal_pool_count": 500,
"influence": 0.3,
"monetary_expand_rate": 0.003,
"treasury_growth_rate": 0.2,
"decentralisation": 0,
"protocol_major": 6,
"protocol_minor": 0,
"min_utxo_value": "1000000",
"min_pool_cost": "340000000",
"nonce": "1a3be38bcbb7911969283716ad7aa550250226b76a61fc51cc9a9a35d9276d81",
"block_hash": "f144a8264acf4bdfe2e1241170969c930d64ab6b0996a4a45237b623f1dd670e",
"price_mem": 0.0577,
"price_step": 0.0000721,
"max_tx_ex_mem": 10000000,
"max_tx_ex_steps": 10000000000_i64,
"max_block_ex_mem": 50000000,
"max_block_ex_steps": 40000000000_i64,
"max_val_size": 5000,
"collateral_percent": 150,
"max_collateral_inputs": 3,
"coins_per_utxo_size": "4310"
}]);
Mock::given(method("GET"))
.and(path("/epoch_params"))
.and(query_param("_epoch_no", epoch_no.value()))
.respond_with(ResponseTemplate::new(200).set_body_json(&mock_response))
.mount(&mock_server)
.await;
let response = client.get_epoch_params(Some(epoch_no)).await.unwrap();
assert_eq!(response.len(), 1);
assert_eq!(response[0].epoch_no, 321);
assert_eq!(response[0].max_tx_ex_mem.unwrap(), 10000000);
assert_eq!(response[0].max_tx_ex_steps.unwrap(), 10000000000);
assert_eq!(response[0].collateral_percent.unwrap(), 150);
assert_eq!(response[0].max_collateral_inputs.unwrap(), 3);
}
#[tokio::test]
async fn test_get_epoch_block_protocols() {
let (mock_server, client) = setup_test_client().await;
let mock_response = json!([{
"proto_major": 6,
"proto_minor": 0,
"blocks": 21600
}]);
Mock::given(method("GET"))
.and(path("/epoch_block_protocols"))
.respond_with(ResponseTemplate::new(200).set_body_json(&mock_response))
.mount(&mock_server)
.await;
let response = client.get_epoch_block_protocols(None).await.unwrap();
assert_eq!(response.len(), 1);
assert_eq!(response[0].proto_major, 6);
assert_eq!(response[0].proto_minor, 0);
assert_eq!(response[0].blocks, 21600);
}
#[tokio::test]
async fn test_get_epoch_block_protocols_with_epoch_no() {
let (mock_server, client) = setup_test_client().await;
let epoch_no = EpochNo::new("321");
let mock_response = json!([{
"proto_major": 6,
"proto_minor": 0,
"blocks": 21600
}]);
Mock::given(method("GET"))
.and(path("/epoch_block_protocols"))
.and(query_param("_epoch_no", epoch_no.value()))
.respond_with(ResponseTemplate::new(200).set_body_json(&mock_response))
.mount(&mock_server)
.await;
let response = client
.get_epoch_block_protocols(Some(epoch_no))
.await
.unwrap();
assert_eq!(response.len(), 1);
assert_eq!(response[0].proto_major, 6);
assert_eq!(response[0].proto_minor, 0);
assert_eq!(response[0].blocks, 21600);
}
#[tokio::test]
async fn test_invalid_epoch_number() {
let (mock_server, client) = setup_test_client().await;
let epoch_no = EpochNo::new("invalid");
Mock::given(method("GET"))
.and(path("/epoch_info"))
.respond_with(ResponseTemplate::new(400).set_body_string("Invalid epoch number format"))
.expect(1)
.mount(&mock_server)
.await;
let err = client
.get_epoch_info(Some(epoch_no), None)
.await
.unwrap_err();
if let koios_sdk::error::Error::Api { status, .. } = err {
assert_eq!(status, 400);
} else {
panic!("Expected API error with status 400");
}
}
#[tokio::test]
async fn test_epoch_not_found() {
let (mock_server, client) = setup_test_client().await;
let epoch_no = EpochNo::new("999999");
Mock::given(method("GET"))
.and(path("/epoch_info"))
.respond_with(ResponseTemplate::new(404).set_body_string(""))
.expect(1)
.mount(&mock_server)
.await;
let err = client
.get_epoch_info(Some(epoch_no), None)
.await
.unwrap_err();
if let koios_sdk::error::Error::Api { status, .. } = err {
assert_eq!(status, 404);
} else {
panic!("Expected API error with status 404");
}
}
#[tokio::test]
async fn test_malformed_epoch_response() {
let (mock_server, client) = setup_test_client().await;
Mock::given(method("GET"))
.and(path("/epoch_info"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"invalid": "response"
})))
.expect(1)
.mount(&mock_server)
.await;
let result = client.get_epoch_info(None, None).await;
match result {
Err(koios_sdk::error::Error::HttpClient(_)) => {
}
other => panic!(
"Expected HttpClient error due to deserialization failure, got {:?}",
other
),
}
}
#[tokio::test]
async fn test_epoch_server_error() {
let (mock_server, client) = setup_test_client().await;
Mock::given(method("GET"))
.and(path("/epoch_params"))
.respond_with(ResponseTemplate::new(500).set_body_string("Internal server error"))
.mount(&mock_server)
.await;
let error = client.get_epoch_params(None).await.unwrap_err();
match error {
koios_sdk::error::Error::Api { status, message } => {
assert_eq!(status, 500);
assert_eq!(message, "Internal server error");
}
_ => panic!("Expected API error"),
}
}
#[tokio::test]
async fn test_epoch_info_and_params_integration() {
let (mock_server, client) = setup_test_client().await;
let epoch_no = EpochNo::new("321");
Mock::given(method("GET"))
.and(path("/epoch_info"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!([{
"epoch_no": 321,
"out_sum": "42000000000",
"fees": "1234567",
"tx_count": 8,
"blk_count": 21600,
"start_time": 1630106091,
"end_time": 1630451690,
"first_block_time": 1630106091,
"last_block_time": 1630451690,
"active_stake": "23456789000",
"total_rewards": "1234567890",
"avg_blk_reward": "57156"
}])))
.expect(1)
.mount(&mock_server)
.await;
Mock::given(method("GET"))
.and(path("/epoch_params"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!([{
"epoch_no": 321,
"min_fee_a": 44,
"min_fee_b": 155381,
"max_block_size": 65536,
"max_tx_size": 16384,
"key_deposit": "2000000",
"pool_deposit": "500000000",
"optimal_pool_count": 500,
"influence": 0.3,
"protocol_major": 6,
"protocol_minor": 0,
"block_hash": "hash123"
}])))
.expect(1)
.mount(&mock_server)
.await;
let (info, params) = tokio::join!(
client.get_epoch_info(Some(epoch_no.clone()), None),
client.get_epoch_params(Some(epoch_no))
);
let info = info.unwrap();
let params = params.unwrap();
assert_eq!(info.len(), 1);
assert_eq!(params.len(), 1);
assert_eq!(info[0].epoch_no, params[0].epoch_no);
assert_eq!(info[0].blk_count, 21600);
assert_eq!(info[0].tx_count, 8);
assert_eq!(info[0].fees, "1234567");
assert_eq!(params[0].max_block_size.unwrap(), 65536);
assert_eq!(params[0].max_tx_size.unwrap(), 16384);
assert_eq!(params[0].protocol_major.unwrap(), 6);
assert_eq!(params[0].protocol_minor.unwrap(), 0);
}
#[tokio::test]
async fn test_comprehensive_epoch_data() {
let (mock_server, client) = setup_test_client().await;
let epoch_no = EpochNo::new("321");
Mock::given(method("GET"))
.and(path("/epoch_info"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!([{
"epoch_no": 321,
"out_sum": "42000000000",
"fees": "1234567",
"tx_count": 8,
"blk_count": 21600,
"start_time": 1630106091,
"end_time": 1630451690,
"first_block_time": 1630106091,
"last_block_time": 1630451690,
"active_stake": "23456789000",
"total_rewards": "1234567890",
"avg_blk_reward": "57156"
}])))
.expect(1)
.mount(&mock_server)
.await;
Mock::given(method("GET"))
.and(path("/epoch_params"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!([{
"epoch_no": 321,
"min_fee_a": 44,
"min_fee_b": 155381,
"max_block_size": 65536,
"max_tx_size": 16384,
"max_bh_size": 1100,
"key_deposit": "2000000",
"pool_deposit": "500000000",
"max_epoch": 18,
"optimal_pool_count": 500,
"influence": 0.3,
"monetary_expand_rate": 0.003,
"treasury_growth_rate": 0.2,
"decentralisation": 0,
"protocol_major": 6,
"protocol_minor": 0,
"min_utxo_value": "1000000",
"min_pool_cost": "340000000",
"nonce": "1a3be38bcbb7911969283716ad7aa550250226b76a61fc51cc9a9a35d9276d81",
"block_hash": "f144a8264acf4bdfe2e1241170969c930d64ab6b0996a4a45237b623f1dd670e",
"price_mem": 0.0577,
"price_step": 0.0000721,
"max_tx_ex_mem": 10000000,
"max_tx_ex_steps": 10000000000_i64,
"max_block_ex_mem": 50000000,
"max_block_ex_steps": 40000000000_i64,
"max_val_size": 5000,
"collateral_percent": 150,
"max_collateral_inputs": 3,
"coins_per_utxo_size": "4310"
}])))
.expect(1)
.mount(&mock_server)
.await;
Mock::given(method("GET"))
.and(path("/epoch_block_protocols"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!([{
"proto_major": 6,
"proto_minor": 0,
"blocks": 21600
}])))
.expect(1)
.mount(&mock_server)
.await;
let (info, params, protocols) = tokio::join!(
client.get_epoch_info(Some(epoch_no.clone()), None),
client.get_epoch_params(Some(epoch_no.clone())),
client.get_epoch_block_protocols(Some(epoch_no))
);
let info = info.unwrap();
let params = params.unwrap();
let protocols = protocols.unwrap();
assert_eq!(info[0].epoch_no, 321);
assert_eq!(params[0].epoch_no, 321);
assert_eq!(info[0].blk_count, protocols[0].blocks);
assert_eq!(params[0].protocol_major.unwrap(), protocols[0].proto_major);
assert_eq!(params[0].protocol_minor.unwrap(), protocols[0].proto_minor);
assert_eq!(info[0].tx_count, 8);
assert_eq!(info[0].blk_count, 21600);
assert_eq!(info[0].active_stake.as_ref().unwrap(), "23456789000");
assert_eq!(params[0].min_fee_a.unwrap(), 44);
assert_eq!(params[0].min_fee_b.unwrap(), 155381);
assert_eq!(params[0].optimal_pool_count.unwrap(), 500);
assert_eq!(protocols[0].proto_major, 6);
assert_eq!(protocols[0].proto_minor, 0);
assert_eq!(protocols[0].blocks, 21600);
}