syncular-testkit 0.1.0

Rust-first test fixtures, in-memory app server, and assertions for Syncular apps.
Documentation
use serde_json::Value;
use std::time::Duration;
use syncular_runtime::client::SyncularClient;
use syncular_runtime::diesel_sqlite::DieselSqliteStore;
use syncular_runtime::store::{ConflictSummary, OutboxSummary};
use syncular_runtime::transport::{SyncAuthHeaders, SyncTransport};

use crate::app_server::{AppTestServer, AppTestServerCommit};
use crate::http::TestHttpRequest;
use crate::transport::{BlobUploadRecord, TestTransportHandle};

pub fn assert_outbox_empty<T>(
    client: &mut SyncularClient<DieselSqliteStore, T>,
) -> Vec<OutboxSummary>
where
    T: SyncTransport,
{
    let summaries = client.outbox_summaries().expect("outbox summaries");
    assert_eq!(summaries.len(), 0, "expected empty outbox: {summaries:?}");
    summaries
}

pub fn assert_outbox_statuses<T>(
    client: &mut SyncularClient<DieselSqliteStore, T>,
    expected: &[&str],
) -> Vec<OutboxSummary>
where
    T: SyncTransport,
{
    let summaries = client.outbox_summaries().expect("outbox summaries");
    let actual = summaries
        .iter()
        .map(|summary| summary.status.as_str())
        .collect::<Vec<_>>();
    assert_eq!(actual, expected, "unexpected outbox statuses");
    summaries
}

pub fn assert_outbox_count<T>(
    client: &mut SyncularClient<DieselSqliteStore, T>,
    expected: usize,
) -> Vec<OutboxSummary>
where
    T: SyncTransport,
{
    let summaries = client.outbox_summaries().expect("outbox summaries");
    assert_eq!(
        summaries.len(),
        expected,
        "unexpected outbox count: {summaries:?}"
    );
    summaries
}

pub fn assert_latest_outbox_status<T>(
    client: &mut SyncularClient<DieselSqliteStore, T>,
    expected: &str,
) -> OutboxSummary
where
    T: SyncTransport,
{
    let summaries = client.outbox_summaries().expect("outbox summaries");
    let latest = summaries.last().expect("latest outbox summary").clone();
    assert_eq!(latest.status, expected, "unexpected latest outbox status");
    latest
}

pub fn assert_conflict_count<T>(
    client: &mut SyncularClient<DieselSqliteStore, T>,
    expected: usize,
) -> Vec<ConflictSummary>
where
    T: SyncTransport,
{
    let conflicts = client.conflict_summaries().expect("conflict summaries");
    assert_eq!(
        conflicts.len(),
        expected,
        "unexpected conflict count: {conflicts:?}"
    );
    conflicts
}

pub fn assert_no_conflicts<T>(
    client: &mut SyncularClient<DieselSqliteStore, T>,
) -> Vec<ConflictSummary>
where
    T: SyncTransport,
{
    assert_conflict_count(client, 0)
}

pub fn assert_table_row_count<T>(
    client: &mut SyncularClient<DieselSqliteStore, T>,
    table: &str,
    expected: usize,
) -> Vec<Value>
where
    T: SyncTransport,
{
    let rows_json = client.list_table_json(table).expect("table rows");
    let rows: Vec<Value> = serde_json::from_str(&rows_json).expect("table rows json");
    assert_eq!(rows.len(), expected, "unexpected row count for {table}");
    rows
}

pub fn assert_table_has_row<T>(
    client: &mut SyncularClient<DieselSqliteStore, T>,
    table: &str,
    primary_key: &str,
    row_id: &str,
) -> Value
where
    T: SyncTransport,
{
    let rows_json = client.list_table_json(table).expect("table rows");
    let rows: Vec<Value> = serde_json::from_str(&rows_json).expect("table rows json");
    rows.into_iter()
        .find(|row| row.get(primary_key).and_then(Value::as_str) == Some(row_id))
        .unwrap_or_else(|| panic!("expected row {table}.{primary_key}={row_id}"))
}

pub fn assert_blob_upload_queue<T>(
    client: &mut SyncularClient<DieselSqliteStore, T>,
    pending: i64,
    uploading: i64,
    failed: i64,
) where
    T: SyncTransport,
{
    let stats = client.blob_upload_queue_stats().expect("blob queue stats");
    assert_eq!(stats.pending, pending, "unexpected pending blob uploads");
    assert_eq!(
        stats.uploading, uploading,
        "unexpected uploading blob uploads"
    );
    assert_eq!(stats.failed, failed, "unexpected failed blob uploads");
}

pub fn assert_blob_cache<T>(
    client: &mut SyncularClient<DieselSqliteStore, T>,
    count: i64,
    total_bytes: i64,
) where
    T: SyncTransport,
{
    let stats = client.blob_cache_stats().expect("blob cache stats");
    assert_eq!(stats.count, count, "unexpected cached blob count");
    assert_eq!(
        stats.total_bytes, total_bytes,
        "unexpected cached blob bytes"
    );
}

pub fn assert_blob_upload_count(
    handle: &TestTransportHandle,
    expected: usize,
) -> Vec<BlobUploadRecord> {
    let uploads = handle.blob_uploads();
    assert_eq!(
        uploads.len(),
        expected,
        "unexpected blob upload count: {uploads:?}"
    );
    uploads
}

pub fn assert_blob_uploaded(handle: &TestTransportHandle, hash: &str) -> BlobUploadRecord {
    handle
        .blob_uploads()
        .into_iter()
        .find(|upload| upload.blob.hash == hash)
        .unwrap_or_else(|| panic!("expected uploaded blob {hash}"))
}

pub fn assert_app_server_row_count(
    server: &AppTestServer,
    table: &str,
    expected: usize,
) -> Vec<Value> {
    let rows = server.rows(table);
    assert_eq!(
        rows.len(),
        expected,
        "unexpected AppTestServer row count for {table}: {rows:?}"
    );
    rows
}

pub fn assert_app_server_has_row(server: &AppTestServer, table: &str, row_id: &str) -> Value {
    server
        .row(table, row_id)
        .unwrap_or_else(|| panic!("expected AppTestServer row {table}.{row_id}"))
}

pub fn assert_app_server_missing_row(server: &AppTestServer, table: &str, row_id: &str) {
    assert!(
        server.row(table, row_id).is_none(),
        "expected missing AppTestServer row {table}.{row_id}"
    );
}

pub fn assert_app_server_commit_count(
    server: &AppTestServer,
    expected: usize,
    timeout: Duration,
) -> Vec<AppTestServerCommit> {
    let commits = server.wait_for_commit_count(expected, timeout);
    assert_eq!(
        commits.len(),
        expected,
        "unexpected AppTestServer commit count: {commits:?}"
    );
    commits
}

pub fn assert_app_server_auth_header(
    server: &AppTestServer,
    name: &str,
    expected: &str,
) -> SyncAuthHeaders {
    let name = name.to_ascii_lowercase();
    server
        .auth_headers()
        .into_iter()
        .find(|headers| headers.get(&name).map(String::as_str) == Some(expected))
        .unwrap_or_else(|| panic!("expected AppTestServer auth header {name}={expected}"))
}

pub fn assert_http_request_count(
    requests: &[TestHttpRequest],
    expected: usize,
) -> &[TestHttpRequest] {
    assert_eq!(
        requests.len(),
        expected,
        "unexpected HTTP request count: {requests:?}"
    );
    requests
}

pub fn assert_http_request_header(
    request: &TestHttpRequest,
    name: &str,
    expected: &str,
) -> TestHttpRequest {
    assert_eq!(
        request.header(name),
        Some(expected),
        "unexpected HTTP request header {name} on {} {}",
        request.method,
        request.path
    );
    request.clone()
}