Skip to content

Commit 2e2a431

Browse files
authored
test: add tests for RPC API routes (#56)
1 parent 13e2e6d commit 2e2a431

File tree

3 files changed

+159
-12
lines changed

3 files changed

+159
-12
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/net/rpc/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,8 @@ tracing.workspace = true
1717
ethlambda-storage.workspace = true
1818
serde.workspace = true
1919
serde_json.workspace = true
20+
21+
[dev-dependencies]
22+
ethlambda-types.workspace = true
23+
tower = { version = "0.5", features = ["util"] }
24+
http-body-util = "0.1"

crates/net/rpc/src/lib.rs

Lines changed: 151 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,27 @@ pub mod metrics;
77

88
pub async fn start_rpc_server(address: SocketAddr, store: Store) -> Result<(), std::io::Error> {
99
let metrics_router = metrics::start_prometheus_metrics_api();
10+
let api_router = build_api_router(store);
1011

11-
// Create stateful routes first, then convert to stateless by applying state
12-
let api_routes = Router::new()
13-
.route("/lean/v0/states/finalized", get(get_latest_finalized_state))
14-
.route(
15-
"/lean/v0/checkpoints/justified",
16-
get(get_latest_justified_state),
17-
)
18-
.with_state(store);
19-
20-
// Merge stateless routers
21-
let app = Router::new().merge(metrics_router).merge(api_routes);
12+
let app = Router::new().merge(metrics_router).merge(api_router);
2213

23-
// Start the axum app
2414
let listener = tokio::net::TcpListener::bind(address).await?;
2515
axum::serve(listener, app).await?;
2616

2717
Ok(())
2818
}
2919

20+
/// Build the API router with the given store.
21+
fn build_api_router(store: Store) -> Router {
22+
Router::new()
23+
.route("/lean/v0/states/finalized", get(get_latest_finalized_state))
24+
.route(
25+
"/lean/v0/checkpoints/justified",
26+
get(get_latest_justified_state),
27+
)
28+
.with_state(store)
29+
}
30+
3031
async fn get_latest_finalized_state(
3132
axum::extract::State(store): axum::extract::State<Store>,
3233
) -> impl IntoResponse {
@@ -43,3 +44,141 @@ async fn get_latest_justified_state(
4344
let checkpoint = store.latest_justified();
4445
Json(checkpoint)
4546
}
47+
48+
#[cfg(test)]
49+
mod tests {
50+
use super::*;
51+
use axum::{
52+
body::Body,
53+
http::{Request, StatusCode},
54+
};
55+
use ethlambda_storage::Store;
56+
use ethlambda_types::{
57+
block::{BlockBody, BlockHeader},
58+
primitives::TreeHash,
59+
state::{ChainConfig, Checkpoint, JustificationValidators, JustifiedSlots, State},
60+
};
61+
use http_body_util::BodyExt;
62+
use serde_json::json;
63+
use tower::ServiceExt;
64+
65+
/// Create a minimal test state for testing.
66+
fn create_test_state() -> State {
67+
let genesis_header = BlockHeader {
68+
slot: 0,
69+
proposer_index: 0,
70+
parent_root: ethlambda_types::primitives::H256::ZERO,
71+
state_root: ethlambda_types::primitives::H256::ZERO,
72+
body_root: BlockBody::default().tree_hash_root(),
73+
};
74+
75+
let genesis_checkpoint = Checkpoint {
76+
root: ethlambda_types::primitives::H256::ZERO,
77+
slot: 0,
78+
};
79+
80+
State {
81+
config: ChainConfig { genesis_time: 1000 },
82+
slot: 0,
83+
latest_block_header: genesis_header,
84+
latest_justified: genesis_checkpoint,
85+
latest_finalized: genesis_checkpoint,
86+
historical_block_hashes: Default::default(),
87+
justified_slots: JustifiedSlots::with_capacity(0).unwrap(),
88+
validators: Default::default(),
89+
justifications_roots: Default::default(),
90+
justifications_validators: JustificationValidators::with_capacity(0).unwrap(),
91+
}
92+
}
93+
94+
#[tokio::test]
95+
async fn test_get_latest_justified_checkpoint() {
96+
let state = create_test_state();
97+
let store = Store::from_genesis(state);
98+
99+
let app = build_api_router(store.clone());
100+
101+
let response = app
102+
.oneshot(
103+
Request::builder()
104+
.uri("/lean/v0/checkpoints/justified")
105+
.body(Body::empty())
106+
.unwrap(),
107+
)
108+
.await
109+
.unwrap();
110+
111+
assert_eq!(response.status(), StatusCode::OK);
112+
113+
let body = response.into_body().collect().await.unwrap().to_bytes();
114+
let checkpoint: serde_json::Value = serde_json::from_slice(&body).unwrap();
115+
116+
// The justified checkpoint should match the store's latest justified
117+
let expected = store.latest_justified();
118+
assert_eq!(
119+
checkpoint,
120+
json!({
121+
"slot": expected.slot,
122+
"root": format!("{:#x}", expected.root)
123+
})
124+
);
125+
}
126+
127+
#[tokio::test]
128+
async fn test_get_latest_finalized_state() {
129+
let state = create_test_state();
130+
let store = Store::from_genesis(state);
131+
132+
// Get the expected state from the store to build expected JSON
133+
let finalized = store.latest_finalized();
134+
let expected_state = store.get_state(&finalized.root).unwrap();
135+
136+
let app = build_api_router(store);
137+
138+
let response = app
139+
.oneshot(
140+
Request::builder()
141+
.uri("/lean/v0/states/finalized")
142+
.body(Body::empty())
143+
.unwrap(),
144+
)
145+
.await
146+
.unwrap();
147+
148+
assert_eq!(response.status(), StatusCode::OK);
149+
150+
let body = response.into_body().collect().await.unwrap().to_bytes();
151+
let returned_state: serde_json::Value = serde_json::from_slice(&body).unwrap();
152+
153+
let header = &expected_state.latest_block_header;
154+
assert_eq!(
155+
returned_state,
156+
json!({
157+
"config": {
158+
"genesis_time": expected_state.config.genesis_time
159+
},
160+
"slot": expected_state.slot,
161+
"latest_block_header": {
162+
"slot": header.slot,
163+
"proposer_index": header.proposer_index,
164+
"parent_root": format!("{:#x}", header.parent_root),
165+
"state_root": format!("{:#x}", header.state_root),
166+
"body_root": format!("{:#x}", header.body_root)
167+
},
168+
"latest_justified": {
169+
"slot": expected_state.latest_justified.slot,
170+
"root": format!("{:#x}", expected_state.latest_justified.root)
171+
},
172+
"latest_finalized": {
173+
"slot": expected_state.latest_finalized.slot,
174+
"root": format!("{:#x}", expected_state.latest_finalized.root)
175+
},
176+
"historical_block_hashes": [],
177+
"justified_slots": "0x01",
178+
"validators": [],
179+
"justifications_roots": [],
180+
"justifications_validators": "0x01"
181+
})
182+
);
183+
}
184+
}

0 commit comments

Comments
 (0)