use crate::circuits::CircuitsApi;
use crate::config::ClientConfig;
use crate::core::CoreApi;
use crate::dcim::DcimApi;
use crate::error::{Error, Result};
use crate::extras::ExtrasApi;
use crate::graphql::GraphqlApi;
use crate::ipam::IpamApi;
use crate::plugins::PluginsApi;
use crate::resource::Resource;
use crate::schema::SchemaApi;
use crate::status::StatusApi;
use crate::tenancy::TenancyApi;
use crate::users::UsersApi;
use crate::virtualization::VirtualizationApi;
use crate::vpn::VpnApi;
use crate::wireless::WirelessApi;
use reqwest::header::{AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderValue, USER_AGENT};
use reqwest::{Method, StatusCode};
use serde::Serialize;
use serde::de::DeserializeOwned;
use serde_json::Value;
use std::sync::Arc;
use std::sync::OnceLock;
use std::time::Duration;
use std::time::Instant;
use tokio::time::sleep;
#[derive(Clone)]
pub struct Client {
config: Arc<ClientConfig>,
http_client: reqwest::Client,
openapi_config: OnceLock<netbox_openapi::apis::configuration::Configuration>,
}
impl Client {
pub fn new(config: ClientConfig) -> Result<Self> {
config.validate()?;
let http_client = if let Some(http_client) = config.http_client.clone() {
http_client
} else {
let mut headers = HeaderMap::new();
headers.insert(
AUTHORIZATION,
HeaderValue::from_str(&format!("Token {}", config.token))
.map_err(|e| Error::Config(format!("Invalid token format: {}", e)))?,
);
headers.insert(
USER_AGENT,
HeaderValue::from_str(&config.user_agent)
.map_err(|e| Error::Config(format!("Invalid user agent: {}", e)))?,
);
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
headers.extend(config.extra_headers.clone());
let builder = reqwest::Client::builder()
.default_headers(headers)
.timeout(config.timeout)
.danger_accept_invalid_certs(!config.verify_ssl);
let builder = if let Some(customize) = &config.http_client_builder {
customize(builder)
} else {
builder
};
builder
.build()
.map_err(|e| Error::Config(format!("Failed to create HTTP client: {}", e)))?
};
Ok(Self {
config: Arc::new(config),
http_client,
openapi_config: OnceLock::new(),
})
}
fn openapi_config_cached(
&self,
) -> &OnceLock<netbox_openapi::apis::configuration::Configuration> {
&self.openapi_config
}
pub fn http_client(&self) -> &reqwest::Client {
&self.http_client
}
pub fn resource(&self, path: impl Into<String>) -> Resource<serde_json::Value> {
Resource::dynamic(self.clone(), path)
}
pub fn dcim(&self) -> DcimApi {
DcimApi::new(self.clone())
}
pub fn circuits(&self) -> CircuitsApi {
CircuitsApi::new(self.clone())
}
pub fn core(&self) -> CoreApi {
CoreApi::new(self.clone())
}
pub fn extras(&self) -> ExtrasApi {
ExtrasApi::new(self.clone())
}
pub fn graphql(&self) -> GraphqlApi {
GraphqlApi::new(self.clone())
}
pub fn ipam(&self) -> IpamApi {
IpamApi::new(self.clone())
}
pub fn plugins(&self) -> PluginsApi {
PluginsApi::new(self.clone())
}
pub fn schema(&self) -> SchemaApi {
SchemaApi::new(self.clone())
}
pub fn status(&self) -> StatusApi {
StatusApi::new(self.clone())
}
pub fn tenancy(&self) -> TenancyApi {
TenancyApi::new(self.clone())
}
pub fn users(&self) -> UsersApi {
UsersApi::new(self.clone())
}
pub fn virtualization(&self) -> VirtualizationApi {
VirtualizationApi::new(self.clone())
}
pub fn vpn(&self) -> VpnApi {
VpnApi::new(self.clone())
}
pub fn wireless(&self) -> WirelessApi {
WirelessApi::new(self.clone())
}
pub fn config(&self) -> &ClientConfig {
&self.config
}
pub fn openapi_config(&self) -> Result<netbox_openapi::apis::configuration::Configuration> {
if let Some(config) = self.openapi_config_cached().get() {
return Ok(config.clone());
}
let client = if let Some(http_client) = self.config.http_client.clone() {
http_client
} else {
let headers = self.config.extra_headers.clone();
let builder = reqwest::Client::builder()
.default_headers(headers)
.timeout(self.config.timeout)
.danger_accept_invalid_certs(!self.config.verify_ssl);
let builder = if let Some(customize) = &self.config.http_client_builder {
customize(builder)
} else {
builder
};
builder.build().map_err(Error::from)?
};
let config = netbox_openapi::apis::configuration::Configuration {
base_path: self
.config
.base_url
.as_str()
.trim_end_matches('/')
.to_string(),
user_agent: Some(self.config.user_agent.clone()),
client,
basic_auth: None,
oauth_access_token: None,
bearer_access_token: None,
api_key: Some(netbox_openapi::apis::configuration::ApiKey {
prefix: Some("Token".to_string()),
key: self.config.token.clone(),
}),
};
let _ = self.openapi_config_cached().set(config.clone());
Ok(config)
}
pub(crate) async fn get<T>(&self, path: &str) -> Result<T>
where
T: DeserializeOwned,
{
self.request_with_retries(Method::GET, path, None::<&()>)
.await
}
pub(crate) async fn get_with_params<T, Q>(&self, path: &str, query: &Q) -> Result<T>
where
T: DeserializeOwned,
Q: Serialize,
{
self.retry_loop(Method::GET, path, |attempt| async move {
#[cfg(not(feature = "tracing"))]
let _ = attempt;
let mut url = self.build_api_url(path)?;
let query_string = serde_urlencoded::to_string(query)?;
if !query_string.is_empty() {
url.set_query(Some(&query_string));
}
#[cfg(feature = "tracing")]
tracing::debug!(
method = %Method::GET,
path,
attempt,
timeout_ms = self.config.timeout.as_millis() as u64,
verify_ssl = self.config.verify_ssl,
"sending request"
);
#[cfg(feature = "tracing")]
let started = Instant::now();
let response = self
.execute_request(&Method::GET, path, self.http_client.get(url))
.await;
match response {
Ok(response) => {
#[cfg(feature = "tracing")]
tracing::debug!(
method = %Method::GET,
path,
attempt,
status = response.status().as_u16(),
duration_ms = started.elapsed().as_millis() as u64,
"received response"
);
self.handle_response(response).await
}
Err(err) => {
#[cfg(feature = "tracing")]
tracing::warn!(
method = %Method::GET,
path,
attempt,
duration_ms = started.elapsed().as_millis() as u64,
error = %err,
"request send failed"
);
Err(err)
}
}
})
.await
}
pub async fn request_raw(
&self,
method: Method,
path: &str,
body: Option<&Value>,
) -> Result<Value> {
self.retry_loop(method.clone(), path, move |attempt| {
#[cfg(not(feature = "tracing"))]
let _ = attempt;
let method = method.clone();
async move {
let url = self.build_api_url(path)?;
let mut request = self.http_client.request(method.clone(), url);
if let Some(body) = body {
request = request.json(body);
}
#[cfg(feature = "tracing")]
tracing::debug!(
method = %method,
path,
attempt,
timeout_ms = self.config.timeout.as_millis() as u64,
verify_ssl = self.config.verify_ssl,
"sending raw request"
);
#[cfg(feature = "tracing")]
let started = Instant::now();
let response =
self.execute_request(&method, path, request)
.await
.map_err(|err| {
#[cfg(feature = "tracing")]
tracing::warn!(
method = %method,
path,
attempt,
duration_ms = started.elapsed().as_millis() as u64,
error = %err,
"raw request send failed"
);
err
})?;
let status = response.status();
#[cfg(feature = "tracing")]
tracing::debug!(
method = %method,
path,
attempt,
status = status.as_u16(),
duration_ms = started.elapsed().as_millis() as u64,
"received raw response"
);
if status.is_success() {
let body_text = response.text().await.map_err(Error::from)?;
if body_text.trim().is_empty() {
Ok(Value::Null)
} else {
Ok(serde_json::from_str(&body_text)?)
}
} else {
let body_text = response.text().await.unwrap_or_default();
Err(Error::from_response(status, body_text))
}
}
})
.await
}
pub(crate) async fn post<T, B>(&self, path: &str, body: &B) -> Result<T>
where
T: DeserializeOwned,
B: Serialize + ?Sized,
{
self.request(Method::POST, path, Some(body)).await
}
pub(crate) async fn put<T, B>(&self, path: &str, body: &B) -> Result<T>
where
T: DeserializeOwned,
B: Serialize + ?Sized,
{
self.request(Method::PUT, path, Some(body)).await
}
pub(crate) async fn patch<T, B>(&self, path: &str, body: &B) -> Result<T>
where
T: DeserializeOwned,
B: Serialize + ?Sized,
{
self.request(Method::PATCH, path, Some(body)).await
}
pub(crate) async fn delete(&self, path: &str) -> Result<()> {
let url = self.build_api_url(path)?;
#[cfg(feature = "tracing")]
tracing::debug!(
method = %Method::DELETE,
path,
timeout_ms = self.config.timeout.as_millis() as u64,
verify_ssl = self.config.verify_ssl,
"sending request"
);
#[cfg(feature = "tracing")]
let started = Instant::now();
let response = self
.execute_request(&Method::DELETE, path, self.http_client.delete(url))
.await
.map_err(|err| {
#[cfg(feature = "tracing")]
tracing::warn!(
method = %Method::DELETE,
path,
duration_ms = started.elapsed().as_millis() as u64,
error = %err,
"request send failed"
);
err
})?;
#[cfg(feature = "tracing")]
tracing::debug!(
method = %Method::DELETE,
path,
status = response.status().as_u16(),
duration_ms = started.elapsed().as_millis() as u64,
"received response"
);
if response.status().is_success() || response.status() == StatusCode::NO_CONTENT {
Ok(())
} else {
let status = response.status();
let body = response.text().await.unwrap_or_default();
Err(Error::from_response(status, body))
}
}
pub(crate) async fn delete_with_body<B>(&self, path: &str, body: &B) -> Result<()>
where
B: Serialize + ?Sized,
{
let url = self.build_api_url(path)?;
#[cfg(feature = "tracing")]
tracing::debug!(
method = %Method::DELETE,
path,
timeout_ms = self.config.timeout.as_millis() as u64,
verify_ssl = self.config.verify_ssl,
"sending request with body"
);
#[cfg(feature = "tracing")]
let started = Instant::now();
let response = self
.execute_request(
&Method::DELETE,
path,
self.http_client.delete(url).json(body),
)
.await
.map_err(|err| {
#[cfg(feature = "tracing")]
tracing::warn!(
method = %Method::DELETE,
path,
duration_ms = started.elapsed().as_millis() as u64,
error = %err,
"request send failed"
);
err
})?;
#[cfg(feature = "tracing")]
tracing::debug!(
method = %Method::DELETE,
path,
status = response.status().as_u16(),
duration_ms = started.elapsed().as_millis() as u64,
"received response"
);
if response.status().is_success() || response.status() == StatusCode::NO_CONTENT {
Ok(())
} else {
let status = response.status();
let body = response.text().await.unwrap_or_default();
Err(Error::from_response(status, body))
}
}
async fn request<T, B>(&self, method: Method, path: &str, body: Option<&B>) -> Result<T>
where
T: DeserializeOwned,
B: Serialize + ?Sized,
{
self.request_with_retries(method, path, body).await
}
async fn request_with_retries<T, B>(
&self,
method: Method,
path: &str,
body: Option<&B>,
) -> Result<T>
where
T: DeserializeOwned,
B: Serialize + ?Sized,
{
self.retry_loop(method.clone(), path, move |attempt| {
let method = method.clone();
async move { self.request_once(method, path, body, attempt).await }
})
.await
}
#[cfg_attr(not(feature = "tracing"), allow(unused_variables))]
async fn request_once<T, B>(
&self,
method: Method,
path: &str,
body: Option<&B>,
attempt: u32,
) -> Result<T>
where
T: DeserializeOwned,
B: Serialize + ?Sized,
{
let url = self.build_api_url(path)?;
let mut request = self.http_client.request(method.clone(), url);
if let Some(body) = body {
request = request.json(body);
}
#[cfg(feature = "tracing")]
tracing::debug!(
method = %method,
path,
attempt,
timeout_ms = self.config.timeout.as_millis() as u64,
verify_ssl = self.config.verify_ssl,
"sending request"
);
#[cfg(feature = "tracing")]
let started = Instant::now();
let response = self
.execute_request(&method, path, request)
.await
.map_err(|err| {
#[cfg(feature = "tracing")]
tracing::warn!(
method = %method,
path,
attempt,
duration_ms = started.elapsed().as_millis() as u64,
error = %err,
"request send failed"
);
err
})?;
#[cfg(feature = "tracing")]
tracing::debug!(
method = %method,
path,
attempt,
status = response.status().as_u16(),
duration_ms = started.elapsed().as_millis() as u64,
"received response"
);
self.handle_response(response).await
}
#[cfg_attr(not(feature = "tracing"), allow(unused_variables))]
async fn retry_loop<T, F, Fut>(&self, method: Method, path: &str, mut operation: F) -> Result<T>
where
F: FnMut(u32) -> Fut,
Fut: std::future::Future<Output = Result<T>>,
{
let mut attempts = 0;
loop {
let result = operation(attempts).await;
match result {
Ok(value) => return Ok(value),
Err(err) => {
#[cfg(feature = "tracing")]
Self::trace_error(&method, path, attempts, &err);
let should_retry =
Self::should_retry(&err, &method, attempts, self.config.max_retries);
if !should_retry {
#[cfg(feature = "tracing")]
tracing::debug!(
method = %method,
path,
attempt = attempts,
max_retries = self.config.max_retries,
"retry skipped"
);
return Err(err);
}
attempts += 1;
let delay = Self::retry_delay(attempts);
#[cfg(feature = "tracing")]
tracing::debug!(
method = %method,
path,
attempt = attempts,
backoff_ms = delay.as_millis() as u64,
"retrying request after backoff"
);
sleep(delay).await;
}
}
}
}
fn should_retry(err: &Error, method: &Method, attempts: u32, max_retries: u32) -> bool {
if attempts >= max_retries {
return false;
}
if method != Method::GET {
return false;
}
match err {
Error::Http(inner) => inner.is_timeout() || inner.is_connect(),
Error::ApiError { status, .. } => *status == 429 || *status >= 500,
_ => false,
}
}
fn build_api_url(&self, path: &str) -> Result<url::Url> {
#[cfg(feature = "tracing")]
tracing::trace!(path, "building request url");
let result = self.config.build_url(path);
#[cfg(feature = "tracing")]
match &result {
Ok(url) => tracing::trace!(path, url = %url, "built request url"),
Err(err) => tracing::warn!(path, error = %err, "failed to build request url"),
}
result
}
fn apply_request_hooks(
&self,
method: &Method,
path: &str,
request: &mut reqwest::Request,
) -> Result<()> {
if let Some(hooks) = &self.config.http_hooks {
hooks.on_request(method, path, request)?;
}
Ok(())
}
fn emit_response_hooks(
&self,
method: &Method,
path: &str,
status: StatusCode,
duration: Duration,
) {
if let Some(hooks) = &self.config.http_hooks {
hooks.on_response(method, path, status, duration);
}
}
fn emit_error_hooks(&self, method: &Method, path: &str, error: &Error, duration: Duration) {
if let Some(hooks) = &self.config.http_hooks {
hooks.on_error(method, path, error, duration);
}
}
async fn execute_request(
&self,
method: &Method,
path: &str,
request: reqwest::RequestBuilder,
) -> Result<reqwest::Response> {
let mut request = request.build().map_err(Error::from)?;
self.apply_request_hooks(method, path, &mut request)?;
let started = Instant::now();
let response = self.http_client.execute(request).await;
let duration = started.elapsed();
match response {
Ok(response) => {
self.emit_response_hooks(method, path, response.status(), duration);
Ok(response)
}
Err(err) => {
let err = Error::from(err);
self.emit_error_hooks(method, path, &err, duration);
Err(err)
}
}
}
#[cfg(feature = "tracing")]
fn trace_error(method: &Method, path: &str, attempt: u32, err: &Error) {
match err {
Error::ApiError {
status, message, ..
} => tracing::warn!(
method = %method,
path,
attempt,
status = *status,
message,
"request failed with api error"
),
Error::Http(inner) => tracing::warn!(
method = %method,
path,
attempt,
timeout = inner.is_timeout(),
connect = inner.is_connect(),
error = %inner,
"request failed with transport error"
),
other => tracing::warn!(
method = %method,
path,
attempt,
error = %other,
"request failed"
),
}
}
fn retry_delay(attempt: u32) -> Duration {
if attempt == 0 {
return Duration::from_millis(0);
}
let exp = attempt.saturating_sub(1);
let backoff_ms = 200u64.saturating_mul(2u64.saturating_pow(exp));
let jitter = (backoff_ms / 4).min(500);
let offset = if jitter == 0 {
0
} else {
Self::jitter_seed(attempt) % (jitter + 1)
};
Duration::from_millis(backoff_ms.saturating_add(offset))
}
fn jitter_seed(attempt: u32) -> u64 {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.subsec_nanos() as u64;
nanos.wrapping_mul(31).wrapping_add(attempt as u64)
}
async fn handle_response<T>(&self, response: reqwest::Response) -> Result<T>
where
T: DeserializeOwned,
{
let status = response.status();
if status.is_success() {
response.json().await.map_err(Error::from)
} else {
let body = response.text().await.unwrap_or_default();
Err(Error::from_response(status, body))
}
}
}
impl std::fmt::Debug for Client {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Client")
.field("base_url", &self.config.base_url)
.field("timeout", &self.config.timeout)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::HttpHooks;
use httpmock::prelude::*;
use reqwest::header::{HeaderName, HeaderValue};
use serde_json::json;
use std::sync::{Arc, Mutex};
#[test]
fn test_client_creation() {
let config = ClientConfig::new("https://netbox.example.com", "test-token");
let client = Client::new(config);
assert!(client.is_ok());
}
#[test]
fn test_client_invalid_config() {
let config = ClientConfig::new("https://netbox.example.com", "");
let client = Client::new(config);
assert!(client.is_err());
}
#[test]
fn test_client_debug() {
let config = ClientConfig::new("https://netbox.example.com", "test-token");
let client = Client::new(config).unwrap();
let debug_str = format!("{:?}", client);
assert!(debug_str.contains("netbox.example.com"));
}
#[test]
fn test_openapi_config() {
let config = ClientConfig::new("https://netbox.example.com", "test-token");
let client = Client::new(config).unwrap();
let openapi_config = client.openapi_config().unwrap();
assert_eq!(openapi_config.base_path, "https://netbox.example.com");
let api_key = openapi_config.api_key.expect("api key should be set");
assert_eq!(api_key.prefix.as_deref(), Some("Token"));
assert_eq!(api_key.key, "test-token");
}
#[test]
fn test_should_retry_respects_limits_and_method() {
let err = Error::ApiError {
status: 500,
message: "server".to_string(),
body: "".to_string(),
};
assert!(Client::should_retry(&err, &Method::GET, 0, 3));
assert!(!Client::should_retry(&err, &Method::POST, 0, 3));
assert!(!Client::should_retry(&err, &Method::GET, 3, 3));
}
#[test]
fn test_should_retry_status_codes() {
let err_429 = Error::ApiError {
status: 429,
message: "rate".to_string(),
body: "".to_string(),
};
let err_500 = Error::ApiError {
status: 500,
message: "server".to_string(),
body: "".to_string(),
};
let err_404 = Error::ApiError {
status: 404,
message: "missing".to_string(),
body: "".to_string(),
};
assert!(Client::should_retry(&err_429, &Method::GET, 0, 1));
assert!(Client::should_retry(&err_500, &Method::GET, 0, 1));
assert!(!Client::should_retry(&err_404, &Method::GET, 0, 1));
}
#[test]
fn test_retry_delay_backoff() {
assert_eq!(Client::retry_delay(0), Duration::from_millis(0));
let delay1 = Client::retry_delay(1).as_millis();
let delay2 = Client::retry_delay(2).as_millis();
let delay3 = Client::retry_delay(3).as_millis();
assert!((200..=700).contains(&delay1));
assert!((400..=900).contains(&delay2));
assert!((800..=1300).contains(&delay3));
}
#[cfg_attr(miri, ignore)]
#[tokio::test]
async fn request_raw_returns_json() {
let server = MockServer::start();
let mock = server.mock(|when, then| {
when.method(GET).path("/api/status/");
then.status(200).json_body(json!({ "ready": true }));
});
let config = ClientConfig::new(server.base_url(), "test-token");
let client = Client::new(config).unwrap();
let value = client
.request_raw(Method::GET, "status/", None)
.await
.unwrap();
assert_eq!(value, json!({ "ready": true }));
mock.assert();
}
#[cfg_attr(miri, ignore)]
#[tokio::test]
async fn request_raw_returns_null_on_empty_body() {
let server = MockServer::start();
let mock = server.mock(|when, then| {
when.method(DELETE).path("/api/test/");
then.status(204);
});
let config = ClientConfig::new(server.base_url(), "test-token");
let client = Client::new(config).unwrap();
let value = client
.request_raw(Method::DELETE, "test/", None)
.await
.unwrap();
assert_eq!(value, Value::Null);
mock.assert();
}
#[cfg_attr(miri, ignore)]
#[tokio::test]
async fn request_raw_includes_extra_headers() {
let server = MockServer::start();
let mock = server.mock(|when, then| {
when.method(GET)
.path("/api/status/")
.header("x-custom", "value");
then.status(200).json_body(json!({ "ready": true }));
});
let config = ClientConfig::new(server.base_url(), "test-token").with_header(
HeaderName::from_static("x-custom"),
HeaderValue::from_static("value"),
);
let client = Client::new(config).unwrap();
let value = client
.request_raw(Method::GET, "status/", None)
.await
.unwrap();
assert_eq!(value, json!({ "ready": true }));
mock.assert();
}
struct AddHeaderHook;
impl HttpHooks for AddHeaderHook {
fn on_request(
&self,
_method: &Method,
_path: &str,
request: &mut reqwest::Request,
) -> Result<()> {
request.headers_mut().insert(
HeaderName::from_static("x-hook"),
HeaderValue::from_static("enabled"),
);
Ok(())
}
}
#[cfg_attr(miri, ignore)]
#[tokio::test]
async fn request_hooks_can_modify_request() {
let server = MockServer::start();
let mock = server.mock(|when, then| {
when.method(GET)
.path("/api/status/")
.header("x-hook", "enabled");
then.status(200).json_body(json!({ "ready": true }));
});
let config =
ClientConfig::new(server.base_url(), "test-token").with_http_hooks(AddHeaderHook);
let client = Client::new(config).unwrap();
let value = client
.request_raw(Method::GET, "status/", None)
.await
.unwrap();
assert_eq!(value, json!({ "ready": true }));
mock.assert();
}
struct ResponseCaptureHook {
statuses: Arc<Mutex<Vec<u16>>>,
}
impl HttpHooks for ResponseCaptureHook {
fn on_response(
&self,
_method: &Method,
_path: &str,
status: StatusCode,
_duration: Duration,
) {
self.statuses.lock().unwrap().push(status.as_u16());
}
}
#[cfg_attr(miri, ignore)]
#[tokio::test]
async fn request_hooks_receive_responses() {
let server = MockServer::start();
let mock = server.mock(|when, then| {
when.method(GET).path("/api/status/");
then.status(200).json_body(json!({ "ready": true }));
});
let statuses = Arc::new(Mutex::new(Vec::new()));
let hook = ResponseCaptureHook {
statuses: statuses.clone(),
};
let config = ClientConfig::new(server.base_url(), "test-token").with_http_hooks(hook);
let client = Client::new(config).unwrap();
let _ = client
.request_raw(Method::GET, "status/", None)
.await
.unwrap();
let captured = statuses.lock().unwrap().clone();
assert_eq!(captured, vec![200]);
mock.assert();
}
#[cfg_attr(miri, ignore)]
#[tokio::test]
async fn prebuilt_http_client_is_used() {
let server = MockServer::start();
let mock = server.mock(|when, then| {
when.method(GET)
.path("/api/status/")
.header("x-prebuilt", "yes");
then.status(200).json_body(json!({ "ready": true }));
});
let mut headers = HeaderMap::new();
headers.insert(
HeaderName::from_static("x-prebuilt"),
HeaderValue::from_static("yes"),
);
let prebuilt = reqwest::Client::builder()
.default_headers(headers)
.build()
.unwrap();
let config = ClientConfig::new(server.base_url(), "test-token").with_http_client(prebuilt);
let client = Client::new(config).unwrap();
let value = client
.request_raw(Method::GET, "status/", None)
.await
.unwrap();
assert_eq!(value, json!({ "ready": true }));
mock.assert();
}
#[cfg(feature = "tracing")]
#[test]
fn tracing_feature_client_creation() {
let config = ClientConfig::new("https://netbox.example.com", "test-token");
let client = Client::new(config);
assert!(client.is_ok());
}
}