tapped 0.3.1

Rust wrapper for the tap ATProto utility
Documentation
//! Integration tests for tapped.
//!
//! These tests require a tap binary to be available on the PATH or in the
//! current directory. They spawn real tap processes and test the full
//! client functionality.
//!
//! Run with: `cargo test --test integration -- --ignored`

use std::sync::atomic::{AtomicU16, Ordering};
use std::time::Duration;
use tapped::{TapClient, TapConfig, TapProcess};

/// Atomic counter for unique test instance assignment to avoid test conflicts.
static TEST_COUNTER: AtomicU16 = AtomicU16::new(15000);

/// Get a unique port and database URL for each test.
///
/// Note: file-based SQLite databases are used because tap's internal
/// components cannot share an in-memory database.
fn unique_test_config() -> (u16, String) {
    let id = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
    let db_url = format!("sqlite:///tmp/tap-test-{}.db", id);
    (id, db_url)
}

#[tokio::test]
#[ignore = "requires tap binary"]
async fn test_process_spawn_default() {
    let (port, db_url) = unique_test_config();
    let config = TapConfig::builder()
        .database_url(db_url)
        .bind(format!("127.0.0.1:{}", port))
        .build();

    let mut process = TapProcess::spawn_default(config)
        .await
        .expect("Failed to spawn tap");

    assert!(process.is_running());

    let client = process.client().expect("Failed to create client");
    client.health().await.expect("Health check failed");

    process.shutdown().await.expect("Shutdown failed");
    assert!(!process.is_running());
}

#[tokio::test]
#[ignore = "requires tap binary"]
async fn test_stats_endpoints() {
    let (port, db_url) = unique_test_config();
    let config = TapConfig::builder()
        .database_url(db_url)
        .bind(format!("127.0.0.1:{}", port))
        .full_network(false)
        .build();

    let process = TapProcess::spawn_default(config)
        .await
        .expect("Failed to spawn tap");
    let client = process.client().expect("Failed to create client");

    let repo_count = client.repo_count().await.expect("repo_count failed");
    assert_eq!(repo_count, 0);

    let record_count = client.record_count().await.expect("record_count failed");
    assert_eq!(record_count, 0);

    let outbox = client.outbox_buffer().await.expect("outbox_buffer failed");
    let _ = outbox;

    let resync = client.resync_buffer().await.expect("resync_buffer failed");
    let _ = resync;

    let cursors = client.cursors().await.expect("cursors failed");
    let _ = cursors;
}

#[tokio::test]
#[ignore = "requires tap binary"]
async fn test_add_and_query_repos() {
    let (port, db_url) = unique_test_config();
    let config = TapConfig::builder()
        .database_url(db_url)
        .bind(format!("127.0.0.1:{}", port))
        .full_network(false)
        .build();

    let process = TapProcess::spawn_default(config)
        .await
        .expect("Failed to spawn tap");
    let client = process.client().expect("Failed to create client");

    client
        .add_repos(&["did:plc:ewvi7nxzyoun6zhxrhs64oiz"])
        .await
        .expect("add_repos failed");

    let count = client.repo_count().await.expect("repo_count failed");
    assert_eq!(count, 1);

    let info = client
        .repo_info("did:plc:ewvi7nxzyoun6zhxrhs64oiz")
        .await
        .expect("repo_info failed");
    assert_eq!(info.did, "did:plc:ewvi7nxzyoun6zhxrhs64oiz");

    client
        .remove_repos(&["did:plc:ewvi7nxzyoun6zhxrhs64oiz"])
        .await
        .expect("remove_repos failed");

    let count = client.repo_count().await.expect("repo_count failed");
    assert_eq!(count, 0);
}

#[tokio::test]
#[ignore = "requires tap binary"]
async fn test_resolve_did() {
    let (port, db_url) = unique_test_config();
    let config = TapConfig::builder()
        .database_url(db_url)
        .bind(format!("127.0.0.1:{}", port))
        .build();

    let process = TapProcess::spawn_default(config)
        .await
        .expect("Failed to spawn tap");
    let client = process.client().expect("Failed to create client");

    let doc = client
        .resolve_did("did:plc:ewvi7nxzyoun6zhxrhs64oiz")
        .await
        .expect("resolve_did failed");

    assert_eq!(doc.id, "did:plc:ewvi7nxzyoun6zhxrhs64oiz");
    assert!(!doc.also_known_as.is_empty());
    assert!(!doc.service.is_empty());
}

#[tokio::test]
#[ignore = "requires tap binary"]
async fn test_channel_connection() {
    let (port, db_url) = unique_test_config();
    let config = TapConfig::builder()
        .database_url(db_url)
        .bind(format!("127.0.0.1:{}", port))
        .startup_timeout(Duration::from_secs(60))
        .build();

    let process = TapProcess::spawn_default(config)
        .await
        .expect("Failed to spawn tap");
    let client = process.client().expect("Failed to create client");

    let _channel = client.channel().await.expect("channel connection failed");
}

#[tokio::test]
#[ignore = "requires tap binary"]
async fn test_graceful_shutdown() {
    let (port, db_url) = unique_test_config();
    let config = TapConfig::builder()
        .database_url(db_url)
        .bind(format!("127.0.0.1:{}", port))
        .startup_timeout(Duration::from_secs(60))
        .shutdown_timeout(Duration::from_secs(2))
        .build();

    let mut process = TapProcess::spawn_default(config)
        .await
        .expect("Failed to spawn tap");

    assert!(process.is_running());

    process.shutdown().await.expect("Shutdown failed");

    assert!(!process.is_running());
}

#[tokio::test]
#[ignore = "requires tap binary"]
async fn test_client_from_url() {
    let (port, db_url) = unique_test_config();
    let config = TapConfig::builder()
        .database_url(db_url)
        .bind(format!("127.0.0.1:{}", port))
        .build();

    let _process = TapProcess::spawn_default(config)
        .await
        .expect("Failed to spawn tap");

    let client =
        TapClient::new(format!("http://127.0.0.1:{}", port)).expect("Failed to create client");

    client.health().await.expect("Health check failed");
}

#[tokio::test]
#[ignore = "requires tap binary"]
async fn test_multiple_collection_filters() {
    let (port, db_url) = unique_test_config();
    let config = TapConfig::builder()
        .database_url(db_url)
        .bind(format!("127.0.0.1:{}", port))
        .startup_timeout(Duration::from_secs(60))
        .collection_filter("app.bsky.feed.post")
        .collection_filter("app.bsky.feed.like")
        .collection_filter("app.bsky.feed.repost")
        .full_network(false)
        .build();

    let process = TapProcess::spawn_default(config)
        .await
        .expect("Failed to spawn tap with multiple filters");
    let client = process.client().expect("Failed to create client");

    client.health().await.expect("Health check failed");
}