use async_trait::async_trait;
use crate::store::{
config::AdapterConfig,
error::Result,
health::{HealthReport, HealthStatus},
};
#[async_trait]
pub trait StorageAdapter: Send + Sync {
type Config: AdapterConfig;
fn name(&self) -> &'static str;
async fn connect(&mut self) -> Result<()>;
async fn disconnect(&mut self) -> Result<()>;
fn is_connected(&self) -> bool;
async fn healthcheck(&self) -> HealthReport;
}
#[async_trait]
pub trait AdapterFactory: Send + Sync {
type Adapter: StorageAdapter;
type Config: AdapterConfig;
async fn build(&self, config: Self::Config) -> Result<Self::Adapter>;
}
#[cfg(test)]
pub(crate) mod mock {
use super::*;
use crate::store::{
config::{AdapterConfig, ConnectionConfig},
error::{Error, Result},
health::{HealthReport, HealthStatus},
};
pub struct MockAdapter {
pub config: ConnectionConfig,
connected: bool,
pub fail_connect: bool,
pub fail_health: bool,
}
impl MockAdapter {
pub fn new(url: &str) -> Self {
Self {
config: ConnectionConfig::new(url),
connected: false,
fail_connect: false,
fail_health: false,
}
}
}
#[async_trait]
impl StorageAdapter for MockAdapter {
type Config = ConnectionConfig;
fn name(&self) -> &'static str {
"mock"
}
async fn connect(&mut self) -> Result<()> {
if self.fail_connect {
return Err(Error::connection("mock: forced connect failure"));
}
self.connected = true;
Ok(())
}
async fn disconnect(&mut self) -> Result<()> {
self.connected = false;
Ok(())
}
fn is_connected(&self) -> bool {
self.connected
}
async fn healthcheck(&self) -> HealthReport {
let status = if self.fail_health {
HealthStatus::Unhealthy { reason: "mock: forced failure".to_string() }
} else if self.connected {
HealthStatus::Healthy
} else {
HealthStatus::Unhealthy { reason: "not connected".to_string() }
};
HealthReport::begin("mock").finish(status)
}
}
pub struct MockAdapterFactory;
#[async_trait]
impl AdapterFactory for MockAdapterFactory {
type Adapter = MockAdapter;
type Config = ConnectionConfig;
async fn build(&self, config: Self::Config) -> Result<Self::Adapter> {
config.validate()?;
let mut adapter = MockAdapter {
config,
connected: false,
fail_connect: false,
fail_health: false,
};
adapter.connect().await?;
Ok(adapter)
}
}
}
#[cfg(test)]
mod tests {
use super::mock::{MockAdapter, MockAdapterFactory};
use super::*;
use crate::store::{
config::{AdapterConfig, ConnectionConfig},
error::Error,
health::HealthStatus,
};
#[tokio::test]
async fn adapter_starts_disconnected() {
let adapter = MockAdapter::new("mock://localhost");
assert!(!adapter.is_connected());
}
#[tokio::test]
async fn connect_sets_connected_state() {
let mut adapter = MockAdapter::new("mock://localhost");
adapter.connect().await.unwrap();
assert!(adapter.is_connected());
}
#[tokio::test]
async fn disconnect_clears_connected_state() {
let mut adapter = MockAdapter::new("mock://localhost");
adapter.connect().await.unwrap();
adapter.disconnect().await.unwrap();
assert!(!adapter.is_connected());
}
#[tokio::test]
async fn reconnect_after_disconnect() {
let mut adapter = MockAdapter::new("mock://localhost");
adapter.connect().await.unwrap();
adapter.disconnect().await.unwrap();
adapter.connect().await.unwrap();
assert!(adapter.is_connected());
}
#[tokio::test]
async fn adapter_name_is_mock() {
let adapter = MockAdapter::new("mock://localhost");
assert_eq!(adapter.name(), "mock");
}
#[tokio::test]
async fn connect_failure_returns_error() {
let mut adapter = MockAdapter::new("mock://localhost");
adapter.fail_connect = true;
let err = adapter.connect().await.unwrap_err();
assert!(matches!(err, Error::Connection(_)));
assert!(!adapter.is_connected());
}
#[tokio::test]
async fn healthcheck_healthy_when_connected() {
let mut adapter = MockAdapter::new("mock://localhost");
adapter.connect().await.unwrap();
let report = adapter.healthcheck().await;
assert_eq!(report.status, HealthStatus::Healthy);
assert_eq!(report.adapter, "mock");
}
#[tokio::test]
async fn healthcheck_unhealthy_when_not_connected() {
let adapter = MockAdapter::new("mock://localhost");
let report = adapter.healthcheck().await;
assert!(!report.status.is_usable());
assert!(report.status.reason().is_some());
}
#[tokio::test]
async fn healthcheck_unhealthy_when_forced() {
let mut adapter = MockAdapter::new("mock://localhost");
adapter.connect().await.unwrap();
adapter.fail_health = true;
let report = adapter.healthcheck().await;
assert!(!report.status.is_usable());
}
#[tokio::test]
async fn healthcheck_report_has_latency() {
let mut adapter = MockAdapter::new("mock://localhost");
adapter.connect().await.unwrap();
let report = adapter.healthcheck().await;
let _ = report.latency; }
#[tokio::test]
async fn factory_builds_and_connects_adapter() {
let factory = MockAdapterFactory;
let config = ConnectionConfig::new("mock://localhost");
let adapter = factory.build(config).await.unwrap();
assert!(adapter.is_connected());
assert_eq!(adapter.name(), "mock");
}
#[tokio::test]
async fn factory_rejects_invalid_config() {
let factory = MockAdapterFactory;
let config = ConnectionConfig::new(""); let err = factory.build(config).await.unwrap_err();
assert!(matches!(err, Error::Configuration(_)));
}
}