use bson::{doc, Document};
use futures::StreamExt;
use mongodb::{options::ClientOptions, Client};
use rigatoni_core::stream::{ChangeStreamConfig, ChangeStreamListener};
use std::time::Duration;
use tokio::time::timeout;
async fn create_test_client() -> Result<Client, Box<dyn std::error::Error>> {
let options = ClientOptions::parse("mongodb://localhost:27017").await?;
Ok(Client::with_options(options)?)
}
#[tokio::test]
#[ignore] async fn test_change_stream_basic() -> Result<(), Box<dyn std::error::Error>> {
let client = create_test_client().await?;
let db = client.database("test_rigatoni");
let collection = db.collection::<Document>("test_events");
collection.drop().await.ok();
let config = ChangeStreamConfig::builder()
.batch_size(10)
.build()
.unwrap();
let mut listener = ChangeStreamListener::new(collection.clone(), config, |token| {
Box::pin(async move {
println!("Resume token: {:?}", token);
Ok(())
})
})
.await?;
let collection_clone = collection.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(100)).await;
for i in 0..5 {
collection_clone.insert_one(doc! { "value": i }).await.ok();
tokio::time::sleep(Duration::from_millis(50)).await;
}
});
let mut count = 0;
let result = timeout(Duration::from_secs(5), async {
while let Some(result) = listener.next().await {
match result {
Ok(ackable) => {
let event = ackable.event_ref();
println!("Received event: {:?}", event.operation);
ackable.ack();
count += 1;
if count >= 5 {
break;
}
}
Err(e) => {
eprintln!("Error: {}", e);
break;
}
}
}
})
.await;
assert!(result.is_ok(), "Should receive events within timeout");
assert_eq!(count, 5, "Should receive 5 insert events");
collection.drop().await.ok();
Ok(())
}
#[tokio::test]
#[ignore] async fn test_change_stream_with_pipeline() -> Result<(), Box<dyn std::error::Error>> {
let client = create_test_client().await?;
let db = client.database("test_rigatoni");
let collection = db.collection::<Document>("test_pipeline");
collection.drop().await.ok();
let pipeline = vec![doc! {
"$match": {
"operationType": "insert"
}
}];
let config = ChangeStreamConfig::builder()
.pipeline(pipeline)
.build()
.unwrap();
let mut listener =
ChangeStreamListener::new(collection.clone(), config, |_| Box::pin(async { Ok(()) }))
.await?;
let collection_clone = collection.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(100)).await;
let doc_id = collection_clone
.insert_one(doc! { "value": 1 })
.await
.unwrap()
.inserted_id;
tokio::time::sleep(Duration::from_millis(50)).await;
collection_clone
.update_one(doc! { "_id": &doc_id }, doc! { "$set": { "value": 2 } })
.await
.ok();
tokio::time::sleep(Duration::from_millis(50)).await;
collection_clone.insert_one(doc! { "value": 3 }).await.ok();
});
let mut insert_count = 0;
let result = timeout(Duration::from_secs(5), async {
while let Some(result) = listener.next().await {
if let Ok(ackable) = result {
let event = ackable.event_ref();
assert!(event.is_insert(), "Should only receive insert events");
ackable.ack();
insert_count += 1;
if insert_count >= 2 {
break;
}
}
}
})
.await;
assert!(result.is_ok());
assert_eq!(insert_count, 2);
collection.drop().await.ok();
Ok(())
}
#[tokio::test]
#[ignore] async fn test_resume_token_persistence() -> Result<(), Box<dyn std::error::Error>> {
use std::sync::{Arc, Mutex};
let client = create_test_client().await?;
let db = client.database("test_rigatoni");
let collection = db.collection::<Document>("test_resume");
collection.drop().await.ok();
let saved_tokens = Arc::new(Mutex::new(Vec::new()));
let saved_tokens_clone = saved_tokens.clone();
let config = ChangeStreamConfig::default();
let mut listener = ChangeStreamListener::new(collection.clone(), config, move |token| {
let tokens = saved_tokens_clone.clone();
Box::pin(async move {
tokens.lock().unwrap().push(token);
Ok(())
})
})
.await?;
let collection_clone = collection.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(100)).await;
for i in 0..3 {
collection_clone.insert_one(doc! { "value": i }).await.ok();
}
});
timeout(Duration::from_secs(3), async {
let mut count = 0;
while let Some(result) = listener.next().await {
if result.is_ok() {
count += 1;
if count >= 3 {
break;
}
}
}
})
.await
.ok();
{
let tokens = saved_tokens.lock().unwrap();
assert!(
tokens.len() >= 3,
"Should have saved at least 3 resume tokens"
);
}
collection.drop().await.ok();
Ok(())
}
#[tokio::test]
#[ignore] async fn test_invalidate_event() -> Result<(), Box<dyn std::error::Error>> {
let client = create_test_client().await?;
let db = client.database("test_rigatoni");
let collection = db.collection::<Document>("test_invalidate");
collection.drop().await.ok();
collection.insert_one(doc! { "init": 1 }).await?;
let config = ChangeStreamConfig::default();
let mut listener =
ChangeStreamListener::new(collection.clone(), config, |_| Box::pin(async { Ok(()) }))
.await?;
let collection_clone = collection.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(200)).await;
collection_clone.drop().await.ok();
});
let result = timeout(Duration::from_secs(5), async {
while let Some(result) = listener.next().await {
match result {
Ok(ackable) => {
let event = ackable.event_ref();
if event.is_invalidate() {
ackable.ack();
return true;
}
ackable.ack();
}
Err(e) => {
println!("Received error: {}", e);
return e.to_string().contains("invalidated")
|| e.to_string().contains("dropped");
}
}
}
false
})
.await;
assert!(result.is_ok());
Ok(())
}
#[test]
fn example_testcontainers_setup() {
}