diff --git a/testing-framework/core/src/nodes/api_client.rs b/testing-framework/core/src/nodes/api_client.rs index 7cc4b7e..377a9f2 100644 --- a/testing-framework/core/src/nodes/api_client.rs +++ b/testing-framework/core/src/nodes/api_client.rs @@ -21,6 +21,14 @@ use tracing::error; pub const DA_GET_TESTING_ENDPOINT_ERROR: &str = "Failed to connect to testing endpoint. The binary was likely built without the 'testing' \ feature. Try: cargo build --workspace --all-features"; +#[derive(Debug, thiserror::Error)] +pub enum ApiClientError { + #[error("{DA_GET_TESTING_ENDPOINT_ERROR}")] + TestingEndpointUnavailable, + #[error(transparent)] + Request(#[from] reqwest::Error), +} + /// Thin async client for node HTTP/testing endpoints. #[derive(Clone)] pub struct ApiClient { @@ -154,6 +162,26 @@ impl ApiClient { } /// POST JSON to the testing API and return the raw response. + pub async fn post_testing_json_response_checked( + &self, + path: &str, + body: &T, + ) -> Result + where + T: Serialize + Sync + ?Sized, + { + let testing_url = self + .testing_url + .as_ref() + .ok_or(ApiClientError::TestingEndpointUnavailable)?; + self.client + .post(Self::join_url(testing_url, path)) + .json(body) + .send() + .await + .map_err(ApiClientError::Request) + } + pub async fn post_testing_json_response( &self, path: &str, @@ -162,27 +190,39 @@ impl ApiClient { where T: Serialize + Sync + ?Sized, { - let testing_url = self - .testing_url - .as_ref() - .expect(DA_GET_TESTING_ENDPOINT_ERROR); - self.client - .post(Self::join_url(testing_url, path)) - .json(body) - .send() - .await + match self.post_testing_json_response_checked(path, body).await { + Ok(resp) => Ok(resp), + Err(ApiClientError::Request(err)) => Err(err), + Err(ApiClientError::TestingEndpointUnavailable) => { + panic!("{DA_GET_TESTING_ENDPOINT_ERROR}") + } + } } /// GET from the testing API and return the raw response. - pub async fn get_testing_response(&self, path: &str) -> reqwest::Result { + pub async fn get_testing_response_checked( + &self, + path: &str, + ) -> Result { let testing_url = self .testing_url .as_ref() - .expect(DA_GET_TESTING_ENDPOINT_ERROR); + .ok_or(ApiClientError::TestingEndpointUnavailable)?; self.client .get(Self::join_url(testing_url, path)) .send() .await + .map_err(ApiClientError::Request) + } + + pub async fn get_testing_response(&self, path: &str) -> reqwest::Result { + match self.get_testing_response_checked(path).await { + Ok(resp) => Ok(resp), + Err(ApiClientError::Request(err)) => Err(err), + Err(ApiClientError::TestingEndpointUnavailable) => { + panic!("{DA_GET_TESTING_ENDPOINT_ERROR}") + } + } } /// Block a peer via the DA testing API. @@ -258,6 +298,19 @@ impl ApiClient { } /// Query DA membership via testing API. + pub async fn da_get_membership_checked( + &self, + session_id: &SessionNumber, + ) -> Result { + self.post_testing_json_response_checked(DA_GET_MEMBERSHIP, session_id) + .await? + .error_for_status() + .map_err(ApiClientError::Request)? + .json() + .await + .map_err(ApiClientError::Request) + } + pub async fn da_get_membership( &self, session_id: &SessionNumber, diff --git a/testing-framework/core/src/nodes/mod.rs b/testing-framework/core/src/nodes/mod.rs index 8ded74c..439cb9c 100644 --- a/testing-framework/core/src/nodes/mod.rs +++ b/testing-framework/core/src/nodes/mod.rs @@ -5,7 +5,7 @@ pub mod validator; use std::sync::LazyLock; -pub use api_client::ApiClient; +pub use api_client::{ApiClient, ApiClientError}; use tempfile::TempDir; pub(crate) const LOGS_PREFIX: &str = "__logs"; diff --git a/testing-framework/core/src/topology/readiness/membership.rs b/testing-framework/core/src/topology/readiness/membership.rs index d913bbf..dca62ae 100644 --- a/testing-framework/core/src/topology/readiness/membership.rs +++ b/testing-framework/core/src/topology/readiness/membership.rs @@ -4,7 +4,7 @@ use reqwest::{Client, Url}; use thiserror::Error; use super::ReadinessCheck; -use crate::topology::deployment::Topology; +use crate::{nodes::ApiClientError, topology::deployment::Topology}; #[derive(Debug, Error)] pub enum MembershipError { @@ -15,7 +15,9 @@ pub enum MembershipError { message: String, }, #[error(transparent)] - Request(#[from] reqwest::Error), + Http(#[from] reqwest::Error), + #[error(transparent)] + ApiClient(#[from] ApiClientError), } #[derive(Debug)] @@ -50,7 +52,7 @@ impl<'a> ReadinessCheck<'a> for MembershipReadiness<'a> { async move { let result = node .api() - .da_get_membership(&self.session) + .da_get_membership_checked(&self.session) .await .map_err(MembershipError::from); NodeMembershipStatus { label, result } @@ -72,7 +74,7 @@ impl<'a> ReadinessCheck<'a> for MembershipReadiness<'a> { async move { let result = node .api() - .da_get_membership(&self.session) + .da_get_membership_checked(&self.session) .await .map_err(MembershipError::from); NodeMembershipStatus { label, result } @@ -164,12 +166,12 @@ pub async fn try_fetch_membership( .json(&session) .send() .await - .map_err(MembershipError::Request)? + .map_err(MembershipError::Http)? .error_for_status() - .map_err(MembershipError::Request)? + .map_err(MembershipError::Http)? .json() .await - .map_err(MembershipError::Request) + .map_err(MembershipError::Http) } #[deprecated(note = "use try_fetch_membership to avoid panics and preserve error details")] @@ -181,7 +183,7 @@ pub async fn fetch_membership( try_fetch_membership(client, base, session) .await .map_err(|err| match err { - MembershipError::Request(source) => source, + MembershipError::Http(source) => source, MembershipError::JoinUrl { base, path, @@ -189,6 +191,7 @@ pub async fn fetch_membership( } => { panic!("failed to join url {base} with path {path}: {message}") } + MembershipError::ApiClient(err) => panic!("{err}"), }) }