use koios_sdk::{types::EpochNo, 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_tip() {
let (mock_server, client) = setup_test_client().await;
let mock_response = json!([{
"hash": "f144a8264acf4bdfe2e1241170969c930d64ab6b0996a4a45237b623f1dd670e",
"epoch_no": 321,
"abs_slot": 53384091,
"epoch_slot": 85691,
"block_no": 7017300,
"block_time": 1630106091
}]);
Mock::given(method("GET"))
.and(path("/tip"))
.respond_with(ResponseTemplate::new(200).set_body_json(&mock_response))
.mount(&mock_server)
.await;
let response = client.get_tip().await.unwrap();
assert_eq!(response.len(), 1);
assert_eq!(
response[0].hash,
"f144a8264acf4bdfe2e1241170969c930d64ab6b0996a4a45237b623f1dd670e"
);
assert_eq!(response[0].epoch_no, 321);
assert_eq!(response[0].abs_slot, 53384091);
assert_eq!(response[0].epoch_slot, 85691);
assert_eq!(response[0].block_no, 7017300);
assert_eq!(response[0].block_time, 1630106091);
}
#[tokio::test]
async fn test_get_genesis() {
let (mock_server, client) = setup_test_client().await;
let mock_response = json!([{
"networkmagic": "764824073",
"networkid": "1",
"epochlength": "432000",
"slotlength": "1",
"maxlovelacesupply": "45000000000000000",
"systemstart": 1506203091,
"activeslotcoeff": "0.05",
"slotsperkesperiod": "129600",
"maxkesrevolutions": "62",
"securityparam": "2160",
"updatequorum": "5",
"alonzogenesis": "{\"lovelacePerUTxOWord\":34482,\"executionPrices\":{...}}"
}]);
Mock::given(method("GET"))
.and(path("/genesis"))
.respond_with(ResponseTemplate::new(200).set_body_json(&mock_response))
.mount(&mock_server)
.await;
let response = client.get_genesis().await.unwrap();
assert_eq!(response.len(), 1);
assert_eq!(response[0].networkmagic, "764824073");
assert_eq!(response[0].networkid, "1");
assert_eq!(response[0].epochlength, "432000");
assert_eq!(response[0].maxlovelacesupply, "45000000000000000");
assert_eq!(response[0].systemstart, 1506203091);
assert_eq!(response[0].activeslotcoeff, "0.05");
assert_eq!(response[0].securityparam, "2160");
}
#[tokio::test]
async fn test_get_totals() {
let (mock_server, client) = setup_test_client().await;
let epoch_no = EpochNo::new("321");
let mock_response = json!([{
"epoch_no": 321,
"circulation": "42000000000000",
"treasury": "1000000000000",
"reward": "500000000000",
"supply": "43500000000000",
"reserves": "1500000000000",
"fees": "100000000",
"deposits_stake": "2000000000",
"deposits_drep": "1000000000",
"deposits_proposal": "500000000"
}]);
Mock::given(method("GET"))
.and(path("/totals"))
.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_totals(Some(epoch_no)).await.unwrap();
assert_eq!(response.len(), 1);
assert_eq!(response[0].epoch_no, 321);
assert_eq!(response[0].circulation, "42000000000000");
assert_eq!(response[0].treasury, "1000000000000");
assert_eq!(response[0].reward, "500000000000");
assert_eq!(response[0].supply, "43500000000000");
assert_eq!(response[0].reserves, "1500000000000");
assert_eq!(response[0].fees, "100000000");
assert_eq!(response[0].deposits_stake, "2000000000");
assert_eq!(response[0].deposits_drep, "1000000000");
assert_eq!(response[0].deposits_proposal, "500000000");
}
#[tokio::test]
async fn test_get_param_updates() {
let (mock_server, client) = setup_test_client().await;
let mock_response = json!([{
"tx_hash": "6ed09ba58a56c6e946668038ba4d3cef8eb97a20cbf76c5970e1402e8a8d6541",
"block_height": 7017300,
"block_time": 1630106091,
"epoch_no": 321,
"data": "{\"protocolVersion\":{\"major\":7,\"minor\":0}}"
}]);
Mock::given(method("GET"))
.and(path("/param_updates"))
.respond_with(ResponseTemplate::new(200).set_body_json(&mock_response))
.mount(&mock_server)
.await;
let response = client.get_param_updates().await.unwrap();
assert_eq!(response.len(), 1);
assert_eq!(
response[0].tx_hash,
"6ed09ba58a56c6e946668038ba4d3cef8eb97a20cbf76c5970e1402e8a8d6541"
);
assert_eq!(response[0].block_height, 7017300);
assert_eq!(response[0].epoch_no, 321);
}
#[tokio::test]
async fn test_get_cli_protocol_params() {
let (mock_server, client) = setup_test_client().await;
let mock_response = json!([{
"collateral_percentage": 150,
"max_block_body_size": 65536,
"max_block_header_size": 1100,
"max_collateral_inputs": 3,
"max_tx_size": 16384,
"max_value_size": 5000,
"min_pool_cost": 340000000,
"min_utxo_value": null,
"monetary_expansion": 0.003,
"pool_pledge_influence": 0.3,
"pool_retire_max_epoch": 18,
"protocol_version": {
"major": 7,
"minor": 0
},
"stake_address_deposit": 2000000,
"stake_pool_deposit": 500000000,
"stake_pool_target_num": 500,
"treasury_cut": 0.2,
"tx_fee_fixed": 155381,
"tx_fee_per_byte": 44,
"utxo_cost_per_byte": 4310
}]);
Mock::given(method("GET"))
.and(path("/cli_protocol_params"))
.respond_with(ResponseTemplate::new(200).set_body_json(&mock_response))
.mount(&mock_server)
.await;
let response = client.get_cli_protocol_params().await.unwrap();
assert_eq!(response.len(), 1);
assert_eq!(response[0].collateral_percentage, 150);
assert_eq!(response[0].max_block_body_size, 65536);
assert_eq!(response[0].max_tx_size, 16384);
assert_eq!(response[0].min_pool_cost, 340000000);
assert_eq!(response[0].monetary_expansion, 0.003);
assert_eq!(response[0].pool_pledge_influence, 0.3);
let protocol_version = &response[0].protocol_version;
assert_eq!(protocol_version.major, 7);
assert_eq!(protocol_version.minor, 0);
}
#[tokio::test]
async fn test_get_reserve_withdrawals() {
let (mock_server, client) = setup_test_client().await;
let mock_response = json!([{
"epoch_no": 321,
"epoch_slot": 85691,
"tx_hash": "6ed09ba58a56c6e946668038ba4d3cef8eb97a20cbf76c5970e1402e8a8d6541",
"block_hash": "f144a8264acf4bdfe2e1241170969c930d64ab6b0996a4a45237b623f1dd670e",
"block_height": 7017300,
"amount": "1000000000",
"stake_address": "stake1u9ylzsgxaa6xctf4juup682ar3juj85n8tx3hthnljg47zctvm3rc",
"earned_epoch": 320,
"spendable_epoch": 322
}]);
Mock::given(method("GET"))
.and(path("/reserve_withdrawals"))
.respond_with(ResponseTemplate::new(200).set_body_json(&mock_response))
.mount(&mock_server)
.await;
let response = client.get_reserve_withdrawals().await.unwrap();
assert_eq!(response.len(), 1);
assert_eq!(response[0].epoch_no, 321);
assert_eq!(response[0].amount, "1000000000");
assert_eq!(response[0].earned_epoch, 320);
assert_eq!(response[0].spendable_epoch, 322);
}
#[tokio::test]
async fn test_get_treasury_withdrawals() {
let (mock_server, client) = setup_test_client().await;
let mock_response = json!([{
"epoch_no": 321,
"epoch_slot": 85691,
"tx_hash": "6ed09ba58a56c6e946668038ba4d3cef8eb97a20cbf76c5970e1402e8a8d6541",
"block_hash": "f144a8264acf4bdfe2e1241170969c930d64ab6b0996a4a45237b623f1dd670e",
"block_height": 7017300,
"amount": "500000000",
"stake_address": "stake1u9ylzsgxaa6xctf4juup682ar3juj85n8tx3hthnljg47zctvm3rc",
"earned_epoch": 320,
"spendable_epoch": 322
}]);
Mock::given(method("GET"))
.and(path("/treasury_withdrawals"))
.respond_with(ResponseTemplate::new(200).set_body_json(&mock_response))
.mount(&mock_server)
.await;
let response = client.get_treasury_withdrawals().await.unwrap();
assert_eq!(response.len(), 1);
assert_eq!(response[0].epoch_no, 321);
assert_eq!(response[0].amount, "500000000");
assert_eq!(response[0].earned_epoch, 320);
assert_eq!(response[0].spendable_epoch, 322);
}
#[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("/totals"))
.and(query_param("_epoch_no", epoch_no.value()))
.respond_with(ResponseTemplate::new(400).set_body_string("Invalid epoch number format"))
.mount(&mock_server)
.await;
let error = client.get_totals(Some(epoch_no)).await.unwrap_err();
match error {
koios_sdk::error::Error::Api { status, message } => {
assert_eq!(status, 400);
assert_eq!(message, "Invalid epoch number format");
}
_ => panic!("Expected API error"),
}
}
#[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("/totals"))
.and(query_param("_epoch_no", epoch_no.value()))
.respond_with(ResponseTemplate::new(404).set_body_string("Epoch not found"))
.mount(&mock_server)
.await;
let error = client.get_totals(Some(epoch_no)).await.unwrap_err();
match error {
koios_sdk::error::Error::Api { status, message } => {
assert_eq!(status, 404);
assert_eq!(message, "Epoch not found");
}
_ => panic!("Expected API error"),
}
}
#[tokio::test]
async fn test_server_error() {
let (mock_server, client) = setup_test_client().await;
Mock::given(method("GET"))
.and(path("/tip"))
.respond_with(ResponseTemplate::new(500).set_body_string("Internal server error"))
.mount(&mock_server)
.await;
let error = client.get_tip().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_malformed_response() {
let (mock_server, client) = setup_test_client().await;
Mock::given(method("GET"))
.and(path("/genesis"))
.respond_with(ResponseTemplate::new(200).set_body_string("invalid json"))
.mount(&mock_server)
.await;
match client.get_genesis().await {
Err(koios_sdk::error::Error::HttpClient(_)) => (),
other => panic!("Expected HttpClient error, got {:?}", other),
}
}
#[tokio::test]
async fn test_tip_and_totals_integration() {
let (mock_server, client) = setup_test_client().await;
let tip_response = json!([{
"hash": "f144a8264acf4bdfe2e1241170969c930d64ab6b0996a4a45237b623f1dd670e",
"epoch_no": 321,
"abs_slot": 53384091,
"epoch_slot": 85691,
"block_no": 7017300,
"block_time": 1630106091
}]);
Mock::given(method("GET"))
.and(path("/tip"))
.respond_with(ResponseTemplate::new(200).set_body_json(&tip_response))
.mount(&mock_server)
.await;
let totals_response = json!([{
"epoch_no": 321,
"circulation": "42000000000000",
"treasury": "1000000000000",
"reward": "500000000000",
"supply": "43500000000000",
"reserves": "1500000000000",
"fees": "100000000",
"deposits_stake": "2000000000",
"deposits_drep": "1000000000",
"deposits_proposal": "500000000"
}]);
Mock::given(method("GET"))
.and(path("/totals"))
.and(query_param("_epoch_no", "321"))
.respond_with(ResponseTemplate::new(200).set_body_json(&totals_response))
.mount(&mock_server)
.await;
let (tip, totals) = tokio::join!(
client.get_tip(),
client.get_totals(Some(EpochNo::new("321")))
);
let tip = tip.unwrap();
let totals = totals.unwrap();
assert_eq!(tip[0].epoch_no, totals[0].epoch_no);
assert_eq!(tip[0].epoch_no, 321);
}
#[tokio::test]
async fn test_network_parameters_integration() {
let (mock_server, client) = setup_test_client().await;
let genesis_response = json!([{
"networkmagic": "764824073",
"networkid": "1",
"epochlength": "432000",
"slotlength": "1",
"maxlovelacesupply": "45000000000000000",
"systemstart": 1506203091,
"activeslotcoeff": "0.05",
"slotsperkesperiod": "129600",
"maxkesrevolutions": "62",
"securityparam": "2160",
"updatequorum": "5",
"alonzogenesis": "{\"lovelacePerUTxOWord\":34482}"
}]);
Mock::given(method("GET"))
.and(path("/genesis"))
.respond_with(ResponseTemplate::new(200).set_body_json(&genesis_response))
.mount(&mock_server)
.await;
let params_response = json!([{
"collateral_percentage": 150,
"max_block_body_size": 65536,
"max_block_header_size": 1100,
"max_collateral_inputs": 3,
"max_tx_size": 16384,
"max_value_size": 5000,
"min_pool_cost": 340000000,
"monetary_expansion": 0.003,
"pool_pledge_influence": 0.3,
"pool_retire_max_epoch": 18, "protocol_version": {
"major": 7,
"minor": 0
},
"stake_address_deposit": 2000000,
"stake_pool_deposit": 500000000,
"stake_pool_target_num": 500,
"treasury_cut": 0.2,
"tx_fee_fixed": 155381,
"tx_fee_per_byte": 44,
"utxo_cost_per_byte": 4310
}]);
Mock::given(method("GET"))
.and(path("/cli_protocol_params"))
.respond_with(ResponseTemplate::new(200).set_body_json(¶ms_response))
.mount(&mock_server)
.await;
let (genesis, params) = tokio::join!(client.get_genesis(), client.get_cli_protocol_params());
let genesis = genesis.unwrap();
let params = params.unwrap();
assert_eq!(genesis[0].networkmagic, "764824073");
assert_eq!(genesis[0].networkid, "1");
assert_eq!(genesis[0].maxlovelacesupply, "45000000000000000");
assert_eq!(params[0].max_block_body_size, 65536);
assert_eq!(params[0].max_tx_size, 16384);
assert_eq!(params[0].min_pool_cost, 340000000);
assert_eq!(params[0].pool_retire_max_epoch, 18);
let version = ¶ms[0].protocol_version;
assert_eq!(version.major, 7);
assert_eq!(version.minor, 0);
}
#[tokio::test]
async fn test_withdrawals_integration() {
let (mock_server, client) = setup_test_client().await;
let reserve_response = json!([{
"epoch_no": 321,
"epoch_slot": 85691,
"tx_hash": "6ed09ba58a56c6e946668038ba4d3cef8eb97a20cbf76c5970e1402e8a8d6541",
"block_hash": "f144a8264acf4bdfe2e1241170969c930d64ab6b0996a4a45237b623f1dd670e",
"block_height": 7017300,
"amount": "1000000000",
"stake_address": "stake1u9ylzsgxaa6xctf4juup682ar3juj85n8tx3hthnljg47zctvm3rc",
"earned_epoch": 320,
"spendable_epoch": 322
}]);
Mock::given(method("GET"))
.and(path("/reserve_withdrawals"))
.respond_with(ResponseTemplate::new(200).set_body_json(&reserve_response))
.mount(&mock_server)
.await;
let treasury_response = json!([{
"epoch_no": 321,
"epoch_slot": 85691,
"tx_hash": "7ed09ba58a56c6e946668038ba4d3cef8eb97a20cbf76c5970e1402e8a8d6542",
"block_hash": "f144a8264acf4bdfe2e1241170969c930d64ab6b0996a4a45237b623f1dd670e",
"block_height": 7017300,
"amount": "500000000",
"stake_address": "stake1u9ylzsgxaa6xctf4juup682ar3juj85n8tx3hthnljg47zctvm3rc",
"earned_epoch": 320,
"spendable_epoch": 322
}]);
Mock::given(method("GET"))
.and(path("/treasury_withdrawals"))
.respond_with(ResponseTemplate::new(200).set_body_json(&treasury_response))
.mount(&mock_server)
.await;
let (reserve, treasury) = tokio::join!(
client.get_reserve_withdrawals(),
client.get_treasury_withdrawals()
);
let reserve = reserve.unwrap();
let treasury = treasury.unwrap();
assert_eq!(reserve[0].epoch_no, treasury[0].epoch_no);
assert_eq!(reserve[0].block_hash, treasury[0].block_hash);
assert_eq!(reserve[0].earned_epoch, treasury[0].earned_epoch);
assert_eq!(reserve[0].spendable_epoch, treasury[0].spendable_epoch);
assert_eq!(reserve[0].amount, "1000000000");
assert_eq!(treasury[0].amount, "500000000");
}