rustauth-core 0.2.0

Core types and primitives for RustAuth.
Documentation
use std::future::Future;

use crate::error::RustAuthError;
use crate::options::SecondaryStorage;

pub async fn assert_secondary_storage_contract<S, Fut, Factory>(
    mut storage_factory: Factory,
) -> Result<(), RustAuthError>
where
    S: SecondaryStorage,
    Fut: Future<Output = Result<S, RustAuthError>>,
    Factory: FnMut(&'static str) -> Fut,
{
    assert_set_if_not_exists_is_atomic(&mut storage_factory).await?;
    assert_compare_and_set_is_atomic(&mut storage_factory).await?;
    assert_delete_if_value_is_atomic(&mut storage_factory).await?;
    assert_take_is_atomic(&mut storage_factory).await?;
    Ok(())
}

async fn assert_set_if_not_exists_is_atomic<S, Fut, Factory>(
    storage_factory: &mut Factory,
) -> Result<(), RustAuthError>
where
    S: SecondaryStorage,
    Fut: Future<Output = Result<S, RustAuthError>>,
    Factory: FnMut(&'static str) -> Fut,
{
    let storage = storage_factory("nx").await?;
    assert!(
        storage
            .set_if_not_exists("existing", "first".to_owned(), Some(60))
            .await?,
        "set_if_not_exists should create an absent key",
    );
    assert!(
        !storage
            .set_if_not_exists("existing", "second".to_owned(), Some(60))
            .await?,
        "set_if_not_exists should not overwrite an existing key",
    );
    assert_eq!(storage.get("existing").await?.as_deref(), Some("first"));

    let (first, second) = tokio::join!(
        storage.set_if_not_exists("concurrent", "first".to_owned(), Some(60)),
        storage.set_if_not_exists("concurrent", "second".to_owned(), Some(60)),
    );
    let created = [first?, second?]
        .into_iter()
        .filter(|created| *created)
        .count();
    assert_eq!(
        created, 1,
        "concurrent set_if_not_exists should create exactly once",
    );
    assert!(
        matches!(
            storage.get("concurrent").await?.as_deref(),
            Some("first" | "second")
        ),
        "set_if_not_exists should keep one concurrent payload",
    );

    storage
        .set("ttl-zero-existing", "stale".to_owned(), Some(60))
        .await?;
    assert!(
        !storage
            .set_if_not_exists("ttl-zero-existing", "ignored".to_owned(), Some(0))
            .await?,
        "set_if_not_exists with ttl=0 should not create or overwrite",
    );
    assert_eq!(
        storage.get("ttl-zero-existing").await?.as_deref(),
        Some("stale"),
        "set_if_not_exists with ttl=0 must leave an existing key untouched",
    );
    assert!(
        !storage
            .set_if_not_exists("ttl-zero-absent", "ignored".to_owned(), Some(0))
            .await?,
        "set_if_not_exists with ttl=0 should not create an absent key",
    );
    assert_eq!(storage.get("ttl-zero-absent").await?, None);
    Ok(())
}

async fn assert_compare_and_set_is_atomic<S, Fut, Factory>(
    storage_factory: &mut Factory,
) -> Result<(), RustAuthError>
where
    S: SecondaryStorage,
    Fut: Future<Output = Result<S, RustAuthError>>,
    Factory: FnMut(&'static str) -> Fut,
{
    let storage = storage_factory("cas").await?;
    assert!(
        storage
            .compare_and_set("absent", None, "created".to_owned(), Some(60))
            .await?,
        "compare_and_set should create when expected value is absent",
    );
    assert_eq!(storage.get("absent").await?.as_deref(), Some("created"));
    assert!(
        !storage
            .compare_and_set("absent", None, "wrong".to_owned(), Some(60))
            .await?,
        "compare_and_set should reject absent expectation once key exists",
    );
    assert!(
        storage
            .compare_and_set(
                "absent",
                Some("created".to_owned()),
                "updated".to_owned(),
                Some(60),
            )
            .await?,
        "compare_and_set should replace a matching value",
    );
    assert_eq!(storage.get("absent").await?.as_deref(), Some("updated"));

    storage
        .set("concurrent", "seed".to_owned(), Some(60))
        .await?;
    let (first, second) = tokio::join!(
        storage.compare_and_set(
            "concurrent",
            Some("seed".to_owned()),
            "first".to_owned(),
            Some(60),
        ),
        storage.compare_and_set(
            "concurrent",
            Some("seed".to_owned()),
            "second".to_owned(),
            Some(60),
        ),
    );
    let applied = [first?, second?]
        .into_iter()
        .filter(|applied| *applied)
        .count();
    assert_eq!(
        applied, 1,
        "concurrent compare_and_set should apply exactly once",
    );
    assert!(
        matches!(
            storage.get("concurrent").await?.as_deref(),
            Some("first" | "second")
        ),
        "compare_and_set should keep the winning value",
    );

    storage
        .set("ttl-zero", "stale".to_owned(), Some(60))
        .await?;
    assert!(
        storage
            .compare_and_set(
                "ttl-zero",
                Some("stale".to_owned()),
                "ignored".to_owned(),
                Some(0),
            )
            .await?,
        "ttl=0 compare_and_set should delete a matching key",
    );
    assert_eq!(storage.get("ttl-zero").await?, None);
    Ok(())
}

async fn assert_delete_if_value_is_atomic<S, Fut, Factory>(
    storage_factory: &mut Factory,
) -> Result<(), RustAuthError>
where
    S: SecondaryStorage,
    Fut: Future<Output = Result<S, RustAuthError>>,
    Factory: FnMut(&'static str) -> Fut,
{
    let storage = storage_factory("delete-if").await?;
    assert!(
        !storage.delete_if_value("missing", None).await?,
        "delete_if_value should not delete when expected is absent",
    );
    storage
        .set("existing", "payload".to_owned(), Some(60))
        .await?;
    assert!(
        !storage
            .delete_if_value("existing", Some("wrong".to_owned()))
            .await?,
        "delete_if_value should reject a non-matching expected value",
    );
    assert_eq!(storage.get("existing").await?.as_deref(), Some("payload"));
    assert!(
        storage
            .delete_if_value("existing", Some("payload".to_owned()))
            .await?,
        "delete_if_value should delete a matching value",
    );
    assert_eq!(storage.get("existing").await?, None);

    storage
        .set("concurrent-delete", "seed".to_owned(), Some(60))
        .await?;
    let (first, second) = tokio::join!(
        storage.delete_if_value("concurrent-delete", Some("seed".to_owned())),
        storage.delete_if_value("concurrent-delete", Some("seed".to_owned())),
    );
    let deleted = [first?, second?]
        .into_iter()
        .filter(|deleted| *deleted)
        .count();
    assert_eq!(
        deleted, 1,
        "concurrent delete_if_value should delete exactly once",
    );
    assert_eq!(storage.get("concurrent-delete").await?, None);
    Ok(())
}

async fn assert_take_is_atomic<S, Fut, Factory>(
    storage_factory: &mut Factory,
) -> Result<(), RustAuthError>
where
    S: SecondaryStorage,
    Fut: Future<Output = Result<S, RustAuthError>>,
    Factory: FnMut(&'static str) -> Fut,
{
    let storage = storage_factory("take").await?;
    storage
        .set("one-time", "payload".to_owned(), Some(60))
        .await?;
    let (first, second) = tokio::join!(storage.take("one-time"), storage.take("one-time"));
    let taken = [first?, second?].into_iter().flatten().collect::<Vec<_>>();
    assert_eq!(taken.len(), 1, "concurrent take should return one payload");
    assert_eq!(taken[0], "payload");
    assert_eq!(storage.get("one-time").await?, None);
    Ok(())
}