use anyhow::Result;
use bc_components::ARID;
use bc_envelope::Envelope;
use hubert::{
KvStore,
server::{Server, ServerConfig, ServerKvClient},
};
use tokio::time::{Duration, sleep};
#[tokio::test(flavor = "multi_thread")]
async fn test_server_put_get_roundtrip() -> Result<()> {
bc_components::register_tags();
let config = ServerConfig::default();
let server = Server::new_memory(config.clone());
tokio::spawn(async move { server.run().await });
sleep(Duration::from_millis(100)).await;
let client =
ServerKvClient::new(&format!("http://127.0.0.1:{}", config.port));
let arid = ARID::new();
let envelope = Envelope::new("Test message for server");
let receipt = client
.put(&arid, &envelope, None, false) .await
.map_err(|e| anyhow::anyhow!("{}", e))?;
assert!(!receipt.is_empty(), "Receipt should not be empty");
let retrieved = client
.get(&arid, Some(30), false)
.await
.map_err(|e| anyhow::anyhow!("{}", e))?;
assert!(retrieved.is_some(), "Envelope should be retrieved");
assert_eq!(
retrieved.unwrap(),
envelope,
"Retrieved envelope should match original"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_server_write_once() -> Result<()> {
bc_components::register_tags();
let config = ServerConfig { port: 45680, ..Default::default() };
let server = Server::new_memory(config.clone());
tokio::spawn(async move { server.run().await });
sleep(Duration::from_millis(100)).await;
let client =
ServerKvClient::new(&format!("http://127.0.0.1:{}", config.port));
let arid = ARID::new();
let envelope1 = Envelope::new("First message");
let envelope2 = Envelope::new("Second message");
client
.put(&arid, &envelope1, None, false) .await
.map_err(|e| anyhow::anyhow!("{}", e))?;
let result = client.put(&arid, &envelope2, None, false).await;
assert!(result.is_err(), "Second put should fail");
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_server_get_nonexistent() -> Result<()> {
bc_components::register_tags();
let config = ServerConfig { port: 45681, ..Default::default() };
let server = Server::new_memory(config.clone());
tokio::spawn(async move { server.run().await });
sleep(Duration::from_millis(100)).await;
let client =
ServerKvClient::new(&format!("http://127.0.0.1:{}", config.port));
let arid = ARID::new();
let retrieved = client
.get(&arid, Some(30), false)
.await
.map_err(|e| anyhow::anyhow!("{}", e))?;
assert!(retrieved.is_none(), "Non-existent ARID should return None");
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_server_ttl() -> Result<()> {
bc_components::register_tags();
let config = ServerConfig { port: 45682, ..Default::default() };
let server = Server::new_memory(config.clone());
tokio::spawn(async move { server.run().await });
sleep(Duration::from_millis(100)).await;
let client =
ServerKvClient::new(&format!("http://127.0.0.1:{}", config.port));
let arid = ARID::new();
let envelope = Envelope::new("Message with TTL");
client
.put(&arid, &envelope, Some(1), false) .await
.map_err(|e| anyhow::anyhow!("{}", e))?;
let retrieved = client
.get(&arid, Some(30), false)
.await
.map_err(|e| anyhow::anyhow!("{}", e))?;
assert!(retrieved.is_some(), "Envelope should be available");
sleep(Duration::from_secs(2)).await;
let retrieved = client
.get(&arid, Some(30), false)
.await
.map_err(|e| anyhow::anyhow!("{}", e))?;
assert!(retrieved.is_none(), "Envelope should be expired");
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_server_default_ttl() -> Result<()> {
bc_components::register_tags();
let config = ServerConfig {
port: 45683,
max_ttl: 2, verbose: false,
};
let server = Server::new_memory(config.clone());
tokio::spawn(async move { server.run().await });
sleep(Duration::from_millis(100)).await;
let client =
ServerKvClient::new(&format!("http://127.0.0.1:{}", config.port));
let arid = ARID::new();
let envelope = Envelope::new("Message with default TTL");
client
.put(&arid, &envelope, None, false)
.await
.map_err(|e| anyhow::anyhow!("{}", e))?;
let retrieved = client
.get(&arid, Some(30), false)
.await
.map_err(|e| anyhow::anyhow!("{}", e))?;
assert!(retrieved.is_some(), "Envelope should be available");
sleep(Duration::from_secs(3)).await;
let retrieved = client
.get(&arid, Some(30), false)
.await
.map_err(|e| anyhow::anyhow!("{}", e))?;
assert!(
retrieved.is_none(),
"Envelope should be expired after max_ttl"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_server_ttl_clamping() -> Result<()> {
bc_components::register_tags();
let config = ServerConfig {
port: 45684,
max_ttl: 2, verbose: false,
};
let server = Server::new_memory(config.clone());
tokio::spawn(async move { server.run().await });
sleep(Duration::from_millis(100)).await;
let client =
ServerKvClient::new(&format!("http://127.0.0.1:{}", config.port));
let arid = ARID::new();
let envelope = Envelope::new("Message with clamped TTL");
client
.put(&arid, &envelope, Some(10), false)
.await
.map_err(|e| anyhow::anyhow!("{}", e))?;
let retrieved = client
.get(&arid, Some(30), false)
.await
.map_err(|e| anyhow::anyhow!("{}", e))?;
assert!(retrieved.is_some(), "Envelope should be available");
sleep(Duration::from_secs(3)).await;
let retrieved = client
.get(&arid, Some(30), false)
.await
.map_err(|e| anyhow::anyhow!("{}", e))?;
assert!(
retrieved.is_none(),
"Envelope should be expired after max_ttl (clamped)"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_server_get_timeout() -> Result<()> {
use tokio::time::Instant;
bc_components::register_tags();
let config = ServerConfig { port: 45685, max_ttl: 86400, verbose: false };
let server = Server::new_memory(config.clone());
tokio::spawn(async move { server.run().await });
sleep(Duration::from_millis(100)).await;
let client =
ServerKvClient::new(&format!("http://127.0.0.1:{}", config.port));
let arid = ARID::new();
let start = Instant::now();
let result = client.get(&arid, Some(2), false).await;
let elapsed = start.elapsed();
assert!(
result.is_ok(),
"Get should succeed (not error) even on timeout"
);
assert!(
result.unwrap().is_none(),
"Should return None after timeout"
);
assert!(
elapsed.as_secs() >= 2 && elapsed.as_secs() <= 3,
"Timeout should be ~2 seconds, was {} seconds",
elapsed.as_secs()
);
Ok(())
}