#![allow(dead_code)]
mod builder;
mod health;
mod inner;
pub use builder::ClientBuilder;
pub use health::{
ComponentHealth, HealthResponse, HealthStatus, ReadinessCriteria, ShutdownGuard, ShutdownHandle,
};
use std::sync::Arc;
#[cfg(not(feature = "rest"))]
use std::time::Duration;
use crate::control::{
AccountClient, ApiClientsClient, AuditLogsClient, InvitationsClient, JwksClient, MembersClient,
OrganizationControlClient, OrganizationsClient, TeamsClient, VaultsClient,
};
use crate::vault::VaultClient;
#[derive(Clone)]
pub struct Client {
inner: Arc<inner::ClientInner>,
}
impl Client {
pub fn builder() -> ClientBuilder<builder::NoUrl, builder::NoCredentials> {
ClientBuilder::new()
}
pub fn organization(&self, organization_id: impl Into<String>) -> OrganizationClient {
OrganizationClient {
client: self.clone(),
organization_id: organization_id.into(),
}
}
pub fn url(&self) -> &str {
&self.inner.url
}
pub(crate) fn from_inner(inner: inner::ClientInner) -> Self {
Self {
inner: Arc::new(inner),
}
}
pub(crate) fn inner(&self) -> &inner::ClientInner {
&self.inner
}
#[cfg(feature = "rest")]
pub(crate) fn transport(
&self,
) -> Option<&std::sync::Arc<dyn crate::transport::TransportClient + Send + Sync>> {
self.inner.transport.as_ref()
}
pub fn account(&self) -> AccountClient {
AccountClient::new(self.clone())
}
pub fn jwks(&self) -> JwksClient {
JwksClient::new(self.clone())
}
pub fn organizations(&self) -> OrganizationsClient {
OrganizationsClient::new(self.clone())
}
pub async fn health_check(&self) -> Result<bool, crate::Error> {
#[cfg(feature = "rest")]
{
return match self.inner.control_get::<serde_json::Value>("/livez").await {
Ok(_) => Ok(true),
Err(_) => Ok(false),
};
}
#[cfg(not(feature = "rest"))]
Ok(true)
}
pub async fn health(&self) -> Result<HealthResponse, crate::Error> {
use std::collections::HashMap;
#[cfg(feature = "rest")]
{
let start = std::time::Instant::now();
#[derive(serde::Deserialize)]
struct ServerHealth {
status: Option<String>,
version: Option<String>,
#[serde(default)]
components: HashMap<String, serde_json::Value>,
}
match self.inner.control_get::<ServerHealth>("/healthz").await {
Ok(server_health) => {
let latency = start.elapsed();
let status = match server_health.status.as_deref() {
Some("healthy") | Some("ok") => HealthStatus::Healthy,
Some("degraded") => HealthStatus::Degraded,
_ => HealthStatus::Unhealthy,
};
let components = server_health
.components
.into_iter()
.map(|(name, value)| {
let component_status = value
.get("status")
.and_then(|s| s.as_str())
.map(|s| match s {
"healthy" | "ok" => HealthStatus::Healthy,
"degraded" => HealthStatus::Degraded,
_ => HealthStatus::Unhealthy,
})
.unwrap_or(HealthStatus::Healthy);
(
name,
ComponentHealth {
status: component_status,
message: None,
latency: None,
last_check: chrono::Utc::now(),
},
)
})
.collect();
Ok(HealthResponse {
status,
version: server_health
.version
.unwrap_or_else(|| "unknown".to_string()),
latency,
components,
timestamp: chrono::Utc::now(),
})
}
Err(_) => {
Ok(HealthResponse {
status: HealthStatus::Unhealthy,
version: "unknown".to_string(),
latency: start.elapsed(),
components: HashMap::new(),
timestamp: chrono::Utc::now(),
})
}
}
}
#[cfg(not(feature = "rest"))]
Ok(HealthResponse {
status: HealthStatus::Healthy,
version: env!("CARGO_PKG_VERSION").to_string(),
latency: Duration::from_millis(1),
components: HashMap::new(),
timestamp: chrono::Utc::now(),
})
}
pub async fn wait_ready(&self, timeout: std::time::Duration) -> Result<(), crate::Error> {
let start = std::time::Instant::now();
while start.elapsed() < timeout {
if self.health_check().await.unwrap_or(false) {
return Ok(());
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
Err(crate::Error::timeout(
"Timed out waiting for service readiness",
))
}
pub async fn wait_ready_with(
&self,
timeout: std::time::Duration,
criteria: ReadinessCriteria,
) -> Result<(), crate::Error> {
let start = std::time::Instant::now();
while start.elapsed() < timeout {
let health = self.health().await?;
let mut ready = health.is_healthy();
if let Some(max_latency) = criteria.max_latency {
ready = ready && health.latency <= max_latency;
}
if ready {
return Ok(());
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
Err(crate::Error::timeout(
"Timed out waiting for service readiness",
))
}
pub fn is_shutting_down(&self) -> bool {
self.inner
.shutdown_guard
.as_ref()
.is_some_and(|guard| guard.is_shutting_down())
}
}
impl std::fmt::Debug for Client {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Client")
.field("url", &self.inner.url)
.finish_non_exhaustive()
}
}
#[derive(Clone)]
pub struct OrganizationClient {
client: Client,
organization_id: String,
}
impl OrganizationClient {
pub fn vault(&self, vault_id: impl Into<String>) -> VaultClient {
VaultClient::new(
self.client.clone(),
self.organization_id.clone(),
vault_id.into(),
)
}
pub fn organization_id(&self) -> &str {
&self.organization_id
}
pub fn client(&self) -> &Client {
&self.client
}
pub fn control(&self) -> OrganizationControlClient {
OrganizationControlClient::new(self.client.clone(), self.organization_id.clone())
}
pub fn clients(&self) -> ApiClientsClient {
ApiClientsClient::new(self.client.clone(), self.organization_id.clone())
}
pub fn vaults(&self) -> VaultsClient {
VaultsClient::new(self.client.clone(), self.organization_id.clone())
}
pub fn members(&self) -> MembersClient {
MembersClient::new(self.client.clone(), self.organization_id.clone())
}
pub fn teams(&self) -> TeamsClient {
TeamsClient::new(self.client.clone(), self.organization_id.clone())
}
pub fn invitations(&self) -> InvitationsClient {
InvitationsClient::new(self.client.clone(), self.organization_id.clone())
}
pub fn audit(&self) -> AuditLogsClient {
AuditLogsClient::new(self.client.clone(), self.organization_id.clone())
}
}
impl std::fmt::Debug for OrganizationClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OrganizationClient")
.field("organization_id", &self.organization_id)
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::auth::BearerCredentialsConfig;
use crate::transport::mock::MockTransport;
use std::sync::Arc;
async fn create_test_client() -> Client {
let mock_transport = Arc::new(MockTransport::new());
Client::builder()
.url("https://api.example.com")
.credentials(BearerCredentialsConfig::new("test"))
.build_with_transport(mock_transport)
.await
.unwrap()
}
#[tokio::test]
async fn test_client_url() {
let client = create_test_client().await;
assert_eq!(client.url(), "https://api.example.com");
}
#[tokio::test]
async fn test_client_debug() {
let client = create_test_client().await;
let debug = format!("{:?}", client);
assert!(debug.contains("Client"));
assert!(debug.contains("api.example.com"));
}
#[tokio::test]
async fn test_client_clone() {
let client = create_test_client().await;
let cloned = client.clone();
assert_eq!(client.url(), cloned.url());
}
#[tokio::test]
async fn test_client_organization() {
let client = create_test_client().await;
let org = client.organization("org_test123");
assert_eq!(org.organization_id(), "org_test123");
}
#[tokio::test]
async fn test_organization_client_vault() {
let client = create_test_client().await;
let org = client.organization("org_test");
let vault = org.vault("vlt_test");
assert_eq!(vault.organization_id(), "org_test");
assert_eq!(vault.vault_id(), "vlt_test");
}
#[tokio::test]
async fn test_organization_client_client() {
let client = create_test_client().await;
let org = client.organization("org_test");
let inner_client = org.client();
assert_eq!(inner_client.url(), "https://api.example.com");
}
#[tokio::test]
async fn test_organization_client_debug() {
let client = create_test_client().await;
let org = client.organization("org_test");
let debug = format!("{:?}", org);
assert!(debug.contains("OrganizationClient"));
assert!(debug.contains("org_test"));
}
#[tokio::test]
async fn test_organization_client_control() {
let client = create_test_client().await;
let org = client.organization("org_test");
let control = org.control();
let debug = format!("{:?}", control);
assert!(debug.contains("org_test"));
}
#[tokio::test]
async fn test_organization_client_clients() {
let client = create_test_client().await;
let org = client.organization("org_test");
let clients = org.clients();
let debug = format!("{:?}", clients);
assert!(debug.contains("org_test"));
}
#[tokio::test]
async fn test_client_account() {
let client = create_test_client().await;
let account = client.account();
let debug = format!("{:?}", account);
assert!(debug.contains("AccountClient"));
}
#[tokio::test]
async fn test_client_jwks() {
let client = create_test_client().await;
let jwks = client.jwks();
let debug = format!("{:?}", jwks);
assert!(debug.contains("JwksClient"));
}
#[tokio::test]
async fn test_client_organizations() {
let client = create_test_client().await;
let orgs = client.organizations();
let debug = format!("{:?}", orgs);
assert!(debug.contains("OrganizationsClient"));
}
#[tokio::test]
async fn test_client_is_not_shutting_down() {
let client = create_test_client().await;
assert!(!client.is_shutting_down());
}
#[tokio::test]
async fn test_readiness_criteria_default() {
let criteria = ReadinessCriteria::new();
assert!(criteria.max_latency.is_none());
assert!(!criteria.require_auth);
assert!(!criteria.require_vault);
}
}
#[cfg(all(test, feature = "rest"))]
mod wiremock_tests {
use super::*;
use crate::auth::BearerCredentialsConfig;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
async fn create_mock_client(server: &MockServer) -> Client {
Client::builder()
.url(server.uri())
.insecure()
.credentials(BearerCredentialsConfig::new("test_token"))
.build()
.await
.unwrap()
}
#[tokio::test]
async fn test_health_check_success() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/livez"))
.respond_with(
ResponseTemplate::new(200).set_body_json(serde_json::json!({"status": "ok"})),
)
.mount(&server)
.await;
let client = create_mock_client(&server).await;
let result = client.health_check().await;
assert!(result.is_ok());
assert!(result.unwrap());
}
#[tokio::test]
async fn test_health_check_failure() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/livez"))
.respond_with(ResponseTemplate::new(503))
.mount(&server)
.await;
let client = create_mock_client(&server).await;
let result = client.health_check().await;
assert!(result.is_ok());
assert!(!result.unwrap());
}
#[tokio::test]
async fn test_health_success() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/healthz"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"status": "healthy",
"version": "1.0.0",
"components": {
"database": {"status": "healthy"},
"cache": {"status": "degraded"}
}
})))
.mount(&server)
.await;
let client = create_mock_client(&server).await;
let result = client.health().await;
assert!(result.is_ok());
let health = result.unwrap();
assert_eq!(health.status, HealthStatus::Healthy);
assert_eq!(health.version, "1.0.0");
assert_eq!(health.components.len(), 2);
}
#[tokio::test]
async fn test_health_degraded() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/healthz"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"status": "degraded",
"version": "1.0.0"
})))
.mount(&server)
.await;
let client = create_mock_client(&server).await;
let result = client.health().await;
assert!(result.is_ok());
let health = result.unwrap();
assert_eq!(health.status, HealthStatus::Degraded);
}
#[tokio::test]
async fn test_health_failure() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/healthz"))
.respond_with(ResponseTemplate::new(503))
.mount(&server)
.await;
let client = create_mock_client(&server).await;
let result = client.health().await;
assert!(result.is_ok());
let health = result.unwrap();
assert_eq!(health.status, HealthStatus::Unhealthy);
}
#[tokio::test]
async fn test_wait_ready_success() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/livez"))
.respond_with(
ResponseTemplate::new(200).set_body_json(serde_json::json!({"status": "ok"})),
)
.mount(&server)
.await;
let client = create_mock_client(&server).await;
let result = client.wait_ready(std::time::Duration::from_secs(1)).await;
assert!(result.is_ok());
}
}