Skip to content

Commit f1dffff

Browse files
committed
Added a unit test for the SSV node mock
1 parent 6e583b2 commit f1dffff

File tree

9 files changed

+198
-20
lines changed

9 files changed

+198
-20
lines changed

Cargo.lock

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

tests/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ cb-signer.workspace = true
1313
eyre.workspace = true
1414
lh_types.workspace = true
1515
reqwest.workspace = true
16+
serde.workspace = true
1617
serde_json.workspace = true
1718
tempfile.workspace = true
1819
tokio.workspace = true

tests/data/ssv_valid_node.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"data": [
3+
{
4+
"public_key": "aa370f6250d421d00437b9900407a7ad93b041aeb7259d99b55ab8b163277746680e93e841f87350737bceee46aa104d",
5+
"index": "1311498",
6+
"status": "active_ongoing",
7+
"activation_epoch": "273156",
8+
"exit_epoch": "18446744073709551615",
9+
"owner": "5e33db0b37622f7e6b2f0654aa7b985d854ea9cb",
10+
"committee": [
11+
1,
12+
2,
13+
3,
14+
4
15+
],
16+
"quorum": 0,
17+
"partial_quorum": 0,
18+
"graffiti": "",
19+
"liquidated": false
20+
}
21+
]
22+
}

tests/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub mod mock_relay;
2-
pub mod mock_ssv;
2+
pub mod mock_ssv_node;
3+
pub mod mock_ssv_public;
34
pub mod mock_validator;
45
pub mod utils;

tests/src/mock_ssv_node.rs

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
use std::{net::SocketAddr, sync::Arc};
2+
3+
use alloy::primitives::U256;
4+
use axum::{
5+
extract::{Json, State},
6+
response::Response,
7+
routing::get,
8+
};
9+
use cb_common::{
10+
config::MUXER_HTTP_MAX_LENGTH,
11+
interop::ssv::types::{SSVNodeResponse, SSVNodeValidator},
12+
};
13+
use serde::Deserialize;
14+
use tokio::{net::TcpListener, sync::RwLock, task::JoinHandle};
15+
use tracing::info;
16+
17+
pub const TEST_HTTP_TIMEOUT: u64 = 2;
18+
19+
/// State for the mock server
20+
#[derive(Clone)]
21+
pub struct SsvNodeMockState {
22+
/// List of pubkeys for the mock server to return
23+
pub validators: Arc<RwLock<Vec<SSVNodeValidator>>>,
24+
25+
/// Whether to force a timeout response to simulate a server error
26+
pub force_timeout: Arc<RwLock<bool>>,
27+
}
28+
29+
#[derive(Deserialize)]
30+
struct SsvNodeValidatorsRequestBody {
31+
pub operators: Vec<U256>,
32+
}
33+
34+
/// Creates a simple mock server to simulate the SSV API endpoint under
35+
/// various conditions for testing. Note this ignores
36+
pub async fn create_mock_ssv_node_server(
37+
port: u16,
38+
state: Option<SsvNodeMockState>,
39+
) -> Result<JoinHandle<()>, axum::Error> {
40+
let data = include_str!("../../tests/data/ssv_valid_node.json");
41+
let response =
42+
serde_json::from_str::<SSVNodeResponse>(data).expect("failed to parse test data");
43+
let state = state.unwrap_or(SsvNodeMockState {
44+
validators: Arc::new(RwLock::new(response.data)),
45+
force_timeout: Arc::new(RwLock::new(false)),
46+
});
47+
let router = axum::Router::new()
48+
.route("/v1/validators", get(handle_validators))
49+
.route("/big_data", get(handle_big_data))
50+
.with_state(state)
51+
.into_make_service();
52+
53+
let address = SocketAddr::from(([127, 0, 0, 1], port));
54+
let listener = TcpListener::bind(address).await.map_err(axum::Error::new)?;
55+
let server = axum::serve(listener, router).with_graceful_shutdown(async {
56+
tokio::signal::ctrl_c().await.expect("Failed to listen for shutdown signal");
57+
});
58+
let result = Ok(tokio::spawn(async move {
59+
if let Err(e) = server.await {
60+
eprintln!("Server error: {e}");
61+
}
62+
}));
63+
info!("Mock server started on http://localhost:{port}/");
64+
result
65+
}
66+
67+
/// Returns a valid SSV validators response, or a timeout if requested in
68+
/// the server state
69+
async fn handle_validators(
70+
State(state): State<SsvNodeMockState>,
71+
Json(body): Json<SsvNodeValidatorsRequestBody>,
72+
) -> Response {
73+
// Time out if requested
74+
if *state.force_timeout.read().await {
75+
return handle_timeout().await;
76+
}
77+
78+
// Make sure the request deserialized properly
79+
let _operators = body.operators;
80+
81+
// Generate the response based on the current validators
82+
let response: SSVNodeResponse;
83+
{
84+
let validators = state.validators.read().await;
85+
response = SSVNodeResponse { data: validators.clone() };
86+
}
87+
88+
// Create a valid response
89+
Response::builder()
90+
.status(200)
91+
.header("Content-Type", "application/json")
92+
.body(serde_json::to_string(&response).unwrap().into())
93+
.unwrap()
94+
}
95+
96+
/// Sends a response with a large body - larger than the maximum allowed.
97+
/// Note that hyper overwrites the content-length header automatically, so
98+
/// setting it here wouldn't actually change the value that ultimately
99+
/// gets sent to the server.
100+
async fn handle_big_data() -> Response {
101+
let body = "f".repeat(2 * MUXER_HTTP_MAX_LENGTH);
102+
Response::builder()
103+
.status(200)
104+
.header("Content-Type", "application/text")
105+
.body(body.into())
106+
.unwrap()
107+
}
108+
109+
/// Simulates a timeout by sleeping for a long time
110+
async fn handle_timeout() -> Response {
111+
// Sleep for a long time to simulate a timeout
112+
tokio::time::sleep(std::time::Duration::from_secs(2 * TEST_HTTP_TIMEOUT)).await;
113+
Response::builder()
114+
.status(200)
115+
.header("Content-Type", "application/text")
116+
.body("Timeout response".into())
117+
.unwrap()
118+
}
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ pub const TEST_HTTP_TIMEOUT: u64 = 2;
1616

1717
/// State for the mock server
1818
#[derive(Clone)]
19-
pub struct SsvMockState {
19+
pub struct PublicSsvMockState {
2020
/// List of pubkeys for the mock server to return
2121
pub validators: Arc<RwLock<Vec<SSVPublicValidator>>>,
2222

@@ -26,14 +26,14 @@ pub struct SsvMockState {
2626

2727
/// Creates a simple mock server to simulate the SSV API endpoint under
2828
/// various conditions for testing. Note this ignores
29-
pub async fn create_mock_ssv_server(
29+
pub async fn create_mock_public_ssv_server(
3030
port: u16,
31-
state: Option<SsvMockState>,
31+
state: Option<PublicSsvMockState>,
3232
) -> Result<JoinHandle<()>, axum::Error> {
33-
let data = include_str!("../../tests/data/ssv_valid.json");
33+
let data = include_str!("../../tests/data/ssv_valid_public.json");
3434
let response =
3535
serde_json::from_str::<SSVPublicResponse>(data).expect("failed to parse test data");
36-
let state = state.unwrap_or(SsvMockState {
36+
let state = state.unwrap_or(PublicSsvMockState {
3737
validators: Arc::new(RwLock::new(response.validators)),
3838
force_timeout: Arc::new(RwLock::new(false)),
3939
});
@@ -63,7 +63,7 @@ pub async fn create_mock_ssv_server(
6363
/// Returns a valid SSV validators response, or a timeout if requested in
6464
/// the server state
6565
async fn handle_validators(
66-
State(state): State<SsvMockState>,
66+
State(state): State<PublicSsvMockState>,
6767
Path((_, _)): Path<(String, u64)>,
6868
) -> Response {
6969
// Time out if requested

tests/tests/pbs_mux.rs

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
use std::{collections::HashMap, sync::Arc, time::Duration};
22

3+
use alloy::primitives::U256;
34
use cb_common::{
45
config::{HTTP_TIMEOUT_SECONDS_DEFAULT, MUXER_HTTP_MAX_LENGTH, RuntimeMuxConfig},
5-
interop::ssv::utils::request_ssv_pubkeys_from_public_api,
6+
interop::ssv::utils::{request_ssv_pubkeys_from_public_api, request_ssv_pubkeys_from_ssv_node},
67
signer::random_secret,
78
types::Chain,
89
utils::{ResponseReadError, set_ignore_content_length},
910
};
1011
use cb_pbs::{DefaultBuilderApi, PbsService, PbsState};
1112
use cb_tests::{
1213
mock_relay::{MockRelayState, start_mock_relay_service},
13-
mock_ssv::{SsvMockState, TEST_HTTP_TIMEOUT, create_mock_ssv_server},
14+
mock_ssv_node::create_mock_ssv_node_server,
15+
mock_ssv_public::{PublicSsvMockState, TEST_HTTP_TIMEOUT, create_mock_public_ssv_server},
1416
mock_validator::MockValidator,
1517
utils::{
1618
bls_pubkey_from_hex_unchecked, generate_mock_relay, get_pbs_static_config, setup_test_env,
@@ -25,10 +27,11 @@ use url::Url;
2527

2628
#[tokio::test]
2729
/// Tests that a successful SSV network fetch is handled and parsed properly
28-
async fn test_ssv_network_fetch() -> Result<()> {
30+
/// from the public API
31+
async fn test_ssv_public_network_fetch() -> Result<()> {
2932
// Start the mock server
3033
let port = 30100;
31-
let _server_handle = create_mock_ssv_server(port, None).await?;
34+
let _server_handle = create_mock_public_ssv_server(port, None).await?;
3235
let url =
3336
Url::parse(&format!("http://localhost:{port}/api/v4/test_chain/validators/in_operator/1"))
3437
.unwrap();
@@ -37,11 +40,11 @@ async fn test_ssv_network_fetch() -> Result<()> {
3740
.await?;
3841

3942
// Make sure the response is correct
40-
// NOTE: requires that ssv_data.json dpesn't change
43+
// NOTE: requires that ssv_valid_public.json doesn't change
4144
assert_eq!(response.validators.len(), 3);
4245
let expected_pubkeys = [
4346
bls_pubkey_from_hex_unchecked(
44-
"967ba17a3e7f82a25aa5350ec34d6923e28ad8237b5a41efe2c5e325240d74d87a015bf04634f21900963539c8229b2a",
47+
"aa370f6250d421d00437b9900407a7ad93b041aeb7259d99b55ab8b163277746680e93e841f87350737bceee46aa104d",
4548
),
4649
bls_pubkey_from_hex_unchecked(
4750
"ac769e8cec802e8ffee34de3253be8f438a0c17ee84bdff0b6730280d24b5ecb77ebc9c985281b41ee3bda8663b6658c",
@@ -66,7 +69,8 @@ async fn test_ssv_network_fetch() -> Result<()> {
6669
async fn test_ssv_network_fetch_big_data() -> Result<()> {
6770
// Start the mock server
6871
let port = 30101;
69-
let server_handle = cb_tests::mock_ssv::create_mock_ssv_server(port, None).await?;
72+
let server_handle =
73+
cb_tests::mock_ssv_public::create_mock_public_ssv_server(port, None).await?;
7074
let url = Url::parse(&format!("http://localhost:{port}/big_data")).unwrap();
7175
let response = request_ssv_pubkeys_from_public_api(url, Duration::from_secs(120)).await;
7276

@@ -97,11 +101,11 @@ async fn test_ssv_network_fetch_big_data() -> Result<()> {
97101
async fn test_ssv_network_fetch_timeout() -> Result<()> {
98102
// Start the mock server
99103
let port = 30102;
100-
let state = SsvMockState {
104+
let state = PublicSsvMockState {
101105
validators: Arc::new(RwLock::new(vec![])),
102106
force_timeout: Arc::new(RwLock::new(true)),
103107
};
104-
let server_handle = create_mock_ssv_server(port, Some(state)).await?;
108+
let server_handle = create_mock_public_ssv_server(port, Some(state)).await?;
105109
let url =
106110
Url::parse(&format!("http://localhost:{port}/api/v4/test_chain/validators/in_operator/1"))
107111
.unwrap();
@@ -127,7 +131,7 @@ async fn test_ssv_network_fetch_big_data_without_content_length() -> Result<()>
127131
// Start the mock server
128132
let port = 30103;
129133
set_ignore_content_length(true);
130-
let server_handle = create_mock_ssv_server(port, None).await?;
134+
let server_handle = create_mock_public_ssv_server(port, None).await?;
131135
let url = Url::parse(&format!("http://localhost:{port}/big_data")).unwrap();
132136
let response = request_ssv_pubkeys_from_public_api(url, Duration::from_secs(120)).await;
133137

@@ -152,6 +156,37 @@ async fn test_ssv_network_fetch_big_data_without_content_length() -> Result<()>
152156
Ok(())
153157
}
154158

159+
#[tokio::test]
160+
/// Tests that a successful SSV network fetch is handled and parsed properly
161+
/// from the node API
162+
async fn test_ssv_node_network_fetch() -> Result<()> {
163+
// Start the mock server
164+
let port = 30104;
165+
let _server_handle = create_mock_ssv_node_server(port, None).await?;
166+
let url = Url::parse(&format!("http://localhost:{port}/v1/validators")).unwrap();
167+
let response = request_ssv_pubkeys_from_ssv_node(
168+
url,
169+
U256::from(1),
170+
Duration::from_secs(HTTP_TIMEOUT_SECONDS_DEFAULT),
171+
)
172+
.await?;
173+
174+
// Make sure the response is correct
175+
// NOTE: requires that ssv_valid_node.json doesn't change
176+
assert_eq!(response.data.len(), 1);
177+
let expected_pubkeys = [bls_pubkey_from_hex_unchecked(
178+
"aa370f6250d421d00437b9900407a7ad93b041aeb7259d99b55ab8b163277746680e93e841f87350737bceee46aa104d",
179+
)];
180+
for (i, validator) in response.data.iter().enumerate() {
181+
assert_eq!(validator.public_key, expected_pubkeys[i]);
182+
}
183+
184+
// Clean up the server handle
185+
_server_handle.abort();
186+
187+
Ok(())
188+
}
189+
155190
#[tokio::test]
156191
async fn test_mux() -> Result<()> {
157192
setup_test_env();

tests/tests/pbs_mux_refresh.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use cb_common::{
99
use cb_pbs::{DefaultBuilderApi, PbsService, PbsState};
1010
use cb_tests::{
1111
mock_relay::{MockRelayState, start_mock_relay_service},
12-
mock_ssv::{SsvMockState, create_mock_ssv_server},
12+
mock_ssv_public::{PublicSsvMockState, create_mock_public_ssv_server},
1313
mock_validator::MockValidator,
1414
utils::{generate_mock_relay, get_pbs_static_config, to_pbs_config},
1515
};
@@ -44,14 +44,14 @@ async fn test_auto_refresh() -> Result<()> {
4444
let ssv_api_port = pbs_port + 1;
4545
// Intentionally missing a trailing slash to ensure this is handled properly
4646
let ssv_api_url = Url::parse(&format!("http://localhost:{ssv_api_port}/api/v4"))?;
47-
let mock_ssv_state = SsvMockState {
47+
let mock_ssv_state = PublicSsvMockState {
4848
validators: Arc::new(RwLock::new(vec![SSVPublicValidator {
4949
pubkey: existing_mux_pubkey.clone(),
5050
}])),
5151
force_timeout: Arc::new(RwLock::new(false)),
5252
};
5353
let ssv_server_handle =
54-
create_mock_ssv_server(ssv_api_port, Some(mock_ssv_state.clone())).await?;
54+
create_mock_public_ssv_server(ssv_api_port, Some(mock_ssv_state.clone())).await?;
5555

5656
// Start a default relay for non-mux keys
5757
let default_relay_port = ssv_api_port + 1;

0 commit comments

Comments
 (0)