use async_trait::async_trait;
use rigatoni_core::destination::{Destination, DestinationError, DestinationMetadata};
use rigatoni_core::event::ChangeEvent;
#[derive(Debug, Default)]
pub struct MockDestination {
events: Vec<ChangeEvent>,
flush_count: usize,
close_count: usize,
fail_writes: bool,
simulate_capacity_error: bool,
metadata: Option<DestinationMetadata>,
}
impl MockDestination {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub const fn with_write_failures(mut self) -> Self {
self.fail_writes = true;
self
}
#[must_use]
pub const fn with_capacity_errors(mut self) -> Self {
self.simulate_capacity_error = true;
self
}
#[must_use]
pub fn with_metadata(mut self, metadata: DestinationMetadata) -> Self {
self.metadata = Some(metadata);
self
}
#[must_use]
pub fn events(&self) -> &[ChangeEvent] {
&self.events
}
#[must_use]
pub fn total_events_written(&self) -> usize {
self.events.len()
}
#[must_use]
pub const fn flush_count(&self) -> usize {
self.flush_count
}
#[must_use]
pub const fn close_count(&self) -> usize {
self.close_count
}
pub fn reset(&mut self) {
self.events.clear();
self.flush_count = 0;
self.close_count = 0;
}
}
#[async_trait]
impl Destination for MockDestination {
async fn write_batch(&mut self, events: &[ChangeEvent]) -> Result<(), DestinationError> {
if self.fail_writes {
return Err(DestinationError::write_msg("Simulated write failure", true));
}
if self.simulate_capacity_error {
return Err(DestinationError::capacity(
"Simulated capacity error",
Some(1.0),
Some(std::time::Duration::from_secs(1)),
));
}
self.events.extend_from_slice(events);
Ok(())
}
async fn flush(&mut self) -> Result<(), DestinationError> {
self.flush_count += 1;
Ok(())
}
fn supports_transactions(&self) -> bool {
false
}
async fn close(&mut self) -> Result<(), DestinationError> {
self.close_count += 1;
self.flush().await
}
fn metadata(&self) -> DestinationMetadata {
self.metadata.clone().unwrap_or_else(|| {
DestinationMetadata::new("MockDestination", "mock")
.with_transactions(false)
.with_concurrent_writes(true)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use bson::doc;
use chrono::Utc;
use rigatoni_core::event::{Namespace, OperationType};
fn create_test_event() -> ChangeEvent {
ChangeEvent {
operation: OperationType::Insert,
namespace: Namespace::new("test_db", "test_collection"),
document_key: Some(doc! { "_id": 1 }),
full_document: Some(doc! { "name": "test", "value": 42 }),
update_description: None,
cluster_time: Utc::now(),
resume_token: doc! { "_data": "test_token" },
}
}
#[tokio::test]
async fn test_mock_destination_write_batch() {
let mut dest = MockDestination::new();
let events = vec![create_test_event(), create_test_event()];
dest.write_batch(&events).await.unwrap();
assert_eq!(dest.total_events_written(), 2);
assert_eq!(dest.events().len(), 2);
}
#[tokio::test]
async fn test_mock_destination_flush() {
let mut dest = MockDestination::new();
assert_eq!(dest.flush_count(), 0);
dest.flush().await.unwrap();
assert_eq!(dest.flush_count(), 1);
dest.flush().await.unwrap();
assert_eq!(dest.flush_count(), 2);
}
#[tokio::test]
async fn test_mock_destination_close() {
let mut dest = MockDestination::new();
dest.close().await.unwrap();
assert_eq!(dest.close_count(), 1);
assert_eq!(dest.flush_count(), 1); }
#[tokio::test]
async fn test_mock_destination_empty_batch() {
let mut dest = MockDestination::new();
dest.write_batch(&[]).await.unwrap();
assert_eq!(dest.total_events_written(), 0);
}
#[tokio::test]
async fn test_mock_destination_write_failures() {
let mut dest = MockDestination::new().with_write_failures();
let events = vec![create_test_event()];
let result = dest.write_batch(&events).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, DestinationError::WriteError { .. }));
assert!(err.is_retryable());
}
#[tokio::test]
async fn test_mock_destination_capacity_errors() {
let mut dest = MockDestination::new().with_capacity_errors();
let events = vec![create_test_event()];
let result = dest.write_batch(&events).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, DestinationError::CapacityError { .. }));
assert!(err.is_retryable());
assert_eq!(err.retry_after(), Some(std::time::Duration::from_secs(1)));
}
#[tokio::test]
async fn test_mock_destination_reset() {
let mut dest = MockDestination::new();
dest.write_batch(&[create_test_event()]).await.unwrap();
dest.flush().await.unwrap();
dest.close().await.unwrap();
assert_eq!(dest.total_events_written(), 1);
assert_eq!(dest.flush_count(), 2); assert_eq!(dest.close_count(), 1);
dest.reset();
assert_eq!(dest.total_events_written(), 0);
assert_eq!(dest.flush_count(), 0);
assert_eq!(dest.close_count(), 0);
}
#[tokio::test]
async fn test_destination_metadata() {
let dest = MockDestination::new();
let meta = dest.metadata();
assert_eq!(meta.name, "MockDestination");
assert_eq!(meta.destination_type, "mock");
assert!(!meta.supports_transactions);
assert!(meta.supports_concurrent_writes);
}
#[tokio::test]
async fn test_custom_metadata() {
let custom_meta = DestinationMetadata::new("CustomDest", "custom")
.with_transactions(true)
.with_max_batch_size(1000)
.with_property("region", "us-west-2");
let dest = MockDestination::new().with_metadata(custom_meta);
let meta = dest.metadata();
assert_eq!(meta.name, "CustomDest");
assert_eq!(meta.destination_type, "custom");
assert!(meta.supports_transactions);
assert_eq!(meta.max_batch_size, Some(1000));
assert_eq!(
meta.properties.get("region"),
Some(&"us-west-2".to_string())
);
}
#[test]
fn test_destination_error_retryable() {
assert!(DestinationError::connection_msg("test").is_retryable());
assert!(
!DestinationError::serialization(std::io::Error::other("test"), "test").is_retryable()
);
assert!(DestinationError::write_msg("test", true).is_retryable());
assert!(!DestinationError::write_msg("test", false).is_retryable());
assert!(!DestinationError::configuration("test", None).is_retryable());
assert!(DestinationError::capacity("test", None, None).is_retryable());
}
#[test]
fn test_destination_error_retry_after() {
let duration = std::time::Duration::from_secs(5);
let err = DestinationError::capacity("test", Some(0.9), Some(duration));
assert_eq!(err.retry_after(), Some(duration));
}
#[test]
fn test_destination_metadata_builder() {
let meta = DestinationMetadata::new("S3", "s3")
.with_transactions(false)
.with_max_batch_size(10_000)
.with_concurrent_writes(true)
.with_property("bucket", "my-bucket")
.with_property("region", "us-east-1");
assert_eq!(meta.name, "S3");
assert_eq!(meta.destination_type, "s3");
assert!(!meta.supports_transactions);
assert_eq!(meta.max_batch_size, Some(10_000));
assert!(meta.supports_concurrent_writes);
assert_eq!(
meta.properties.get("bucket"),
Some(&"my-bucket".to_string())
);
assert_eq!(
meta.properties.get("region"),
Some(&"us-east-1".to_string())
);
}
}