use faucet_conformance::{
assert_bookmark_roundtrip, assert_config_schema_valid_value, assert_errors_not_panics,
};
use faucet_source_kinesis::{KinesisCredentials, KinesisSource, KinesisSourceConfig};
use testcontainers::{ContainerAsync, runners::AsyncRunner};
use testcontainers_modules::localstack::LocalStack;
#[test]
fn conformance_config_schema_valid() {
let schema = serde_json::to_value(schemars::schema_for!(KinesisSourceConfig)).unwrap();
assert_config_schema_valid_value(&schema, "faucet-source-kinesis");
}
async fn start_localstack() -> (ContainerAsync<LocalStack>, String) {
use testcontainers::ImageExt;
let image = LocalStack::default().with_env_var("SERVICES", "kinesis");
let container = image.start().await.expect("localstack start");
let port = container
.get_host_port_ipv4(4566)
.await
.expect("localstack port");
(container, format!("http://127.0.0.1:{port}"))
}
fn test_credentials() -> KinesisCredentials {
KinesisCredentials::AccessKey {
access_key_id: "test".into(),
secret_access_key: "test".into(),
session_token: None,
}
}
async fn raw_client(endpoint: &str) -> aws_sdk_kinesis::Client {
faucet_source_kinesis::build_client(Some("us-east-1"), Some(endpoint), &test_credentials())
.await
.expect("client")
}
async fn await_ready(client: &aws_sdk_kinesis::Client) {
for _ in 0..120 {
if client.list_streams().send().await.is_ok() {
return;
}
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
panic!("localstack kinesis never became ready");
}
async fn create_stream(client: &aws_sdk_kinesis::Client, name: &str, shards: i32) {
await_ready(client).await;
client
.create_stream()
.stream_name(name)
.shard_count(shards)
.send()
.await
.expect("create stream");
for _ in 0..60 {
let out = client
.describe_stream_summary()
.stream_name(name)
.send()
.await
.expect("describe");
if out
.stream_description_summary()
.map(|d| d.stream_status() == &aws_sdk_kinesis::types::StreamStatus::Active)
.unwrap_or(false)
{
return;
}
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
panic!("stream {name} never became ACTIVE");
}
async fn put_records(client: &aws_sdk_kinesis::Client, stream: &str, start: usize, n: usize) {
use aws_sdk_kinesis::primitives::Blob;
use aws_sdk_kinesis::types::PutRecordsRequestEntry;
for chunk_start in (start..start + n).step_by(100) {
let entries: Vec<PutRecordsRequestEntry> = (chunk_start
..(chunk_start + 100).min(start + n))
.map(|i| {
PutRecordsRequestEntry::builder()
.partition_key(format!("user-{}", i % 7))
.data(Blob::new(format!("{{\"i\":{i}}}").into_bytes()))
.build()
.expect("entry")
})
.collect();
let out = client
.put_records()
.stream_name(stream)
.set_records(Some(entries))
.send()
.await
.expect("put_records");
assert_eq!(
out.failed_record_count().unwrap_or(0),
0,
"seeding must not throttle"
);
}
}
fn source_config(endpoint: &str, stream: &str) -> KinesisSourceConfig {
let mut cfg = KinesisSourceConfig::new(stream);
cfg.region = Some("us-east-1".into());
cfg.endpoint_url = Some(endpoint.into());
cfg.credentials = test_credentials();
cfg.poll_interval_ms = 200;
cfg.idle_termination_secs = Some(15);
cfg.batch_size = 250;
cfg
}
#[tokio::test(flavor = "multi_thread")]
async fn conformance_bounded_memory() {
let (_container, endpoint) = start_localstack().await;
let client = raw_client(&endpoint).await;
create_stream(&client, "conformance", 3).await;
put_records(&client, "conformance", 0, 5_000).await;
let source = KinesisSource::new(source_config(&endpoint, "conformance"))
.await
.expect("source");
faucet_conformance::assert_bounded_memory(&source, 250, 5_000).await;
}
#[tokio::test(flavor = "multi_thread")]
async fn conformance_bookmark_roundtrip() {
let (_container, endpoint) = start_localstack().await;
let client = raw_client(&endpoint).await;
create_stream(&client, "resume", 3).await;
put_records(&client, "resume", 0, 500).await;
let source = KinesisSource::new(source_config(&endpoint, "resume"))
.await
.expect("source");
assert_bookmark_roundtrip(&source).await;
}
#[tokio::test(flavor = "multi_thread")]
async fn conformance_errors_not_panics() {
let mut cfg = KinesisSourceConfig::new("does-not-exist");
cfg.region = Some("us-east-1".into());
cfg.endpoint_url = Some("http://127.0.0.1:1".into());
cfg.credentials = test_credentials();
cfg.poll_interval_ms = 200;
cfg.idle_termination_secs = Some(1);
cfg.max_messages = Some(10);
let source = KinesisSource::new(cfg).await.expect("source builds lazily");
assert_errors_not_panics(&source).await;
}