openauth-plugins 0.0.5

Official OpenAuth plugin modules.
Documentation
use std::sync::Arc;

use http::{Method, StatusCode};
use openauth_core::db::{DbAdapter, DbValue, Delete, MemoryAdapter, Where};
use openauth_plugins::api_key::{
    api_key_with_options, ApiKeyConfiguration, ApiKeyOptions, ApiKeyStorageMode, API_KEY_MODEL,
};
use serde_json::json;

use super::helpers::{request_json, sign_up, test_router, TestSecondaryStorage};

#[tokio::test]
async fn secondary_storage_mode_does_not_write_database_rows(
) -> Result<(), Box<dyn std::error::Error>> {
    let adapter = Arc::new(MemoryAdapter::new());
    let storage = Arc::new(TestSecondaryStorage::default());
    let router = test_router(
        adapter.clone(),
        api_key_with_options(ApiKeyOptions {
            configuration: ApiKeyConfiguration {
                storage: ApiKeyStorageMode::SecondaryStorage,
                custom_storage: Some(storage.clone()),
                ..ApiKeyConfiguration::default()
            },
        }),
    )?;
    let user = sign_up(&router, "Cid", "cid-api@example.com").await?;
    let created = request_json(
        &router,
        Method::POST,
        "/api/auth/api-key/create",
        json!({"name":"cache-only","expiresIn": 60 * 60 * 24}),
        Some(&user.cookie),
        None,
    )
    .await?;
    assert_eq!(created.status, StatusCode::OK);
    assert_eq!(adapter.records(API_KEY_MODEL).await.len(), 0);
    let key_id = created.body["id"].as_str().ok_or("missing api key id")?;
    assert!(storage
        .ttl_for(&format!("api-key:by-id:{key_id}"))
        .flatten()
        .is_some_and(|ttl| ttl > 0));

    let verified = request_json(
        &router,
        Method::POST,
        "/api/auth/api-key/verify",
        json!({"key": created.body["key"]}),
        None,
        None,
    )
    .await?;
    assert_eq!(verified.status, StatusCode::OK);
    assert_eq!(verified.body["valid"], true);
    Ok(())
}

#[tokio::test]
async fn fallback_storage_keeps_database_as_source_and_invalidates_ref_cache(
) -> Result<(), Box<dyn std::error::Error>> {
    let adapter = Arc::new(MemoryAdapter::new());
    let storage = Arc::new(TestSecondaryStorage::default());
    let router = test_router(
        adapter.clone(),
        api_key_with_options(ApiKeyOptions {
            configuration: ApiKeyConfiguration {
                storage: ApiKeyStorageMode::SecondaryStorage,
                fallback_to_database: true,
                custom_storage: Some(storage.clone()),
                ..ApiKeyConfiguration::default()
            },
        }),
    )?;
    let user = sign_up(&router, "Dom", "dom-api@example.com").await?;
    let created = request_json(
        &router,
        Method::POST,
        "/api/auth/api-key/create",
        json!({"name":"fallback"}),
        Some(&user.cookie),
        None,
    )
    .await?;
    assert_eq!(created.status, StatusCode::OK);
    assert_eq!(adapter.records(API_KEY_MODEL).await.len(), 1);
    assert!(storage
        .deleted_keys()
        .iter()
        .any(|key| key == &format!("api-key:by-ref:{}", user.user_id)));

    let listed = request_json(
        &router,
        Method::GET,
        "/api/auth/api-key/list",
        serde_json::Value::Null,
        Some(&user.cookie),
        None,
    )
    .await?;
    assert_eq!(listed.status, StatusCode::OK);
    assert_eq!(listed.body["total"], 1);
    assert_eq!(listed.body["apiKeys"][0]["name"], "fallback");
    Ok(())
}

#[tokio::test]
async fn fallback_storage_list_reads_existing_ref_cache_before_database(
) -> Result<(), Box<dyn std::error::Error>> {
    let adapter = Arc::new(MemoryAdapter::new());
    let storage = Arc::new(TestSecondaryStorage::default());
    let router = test_router(
        adapter.clone(),
        api_key_with_options(ApiKeyOptions {
            configuration: ApiKeyConfiguration {
                storage: ApiKeyStorageMode::SecondaryStorage,
                fallback_to_database: true,
                custom_storage: Some(storage),
                ..ApiKeyConfiguration::default()
            },
        }),
    )?;
    let user = sign_up(&router, "Kai", "kai-api@example.com").await?;
    let created = request_json(
        &router,
        Method::POST,
        "/api/auth/api-key/create",
        json!({"name":"cached"}),
        Some(&user.cookie),
        None,
    )
    .await?;
    assert_eq!(created.status, StatusCode::OK);
    let key_id = created.body["id"].as_str().ok_or("missing api key id")?;

    let populated = request_json(
        &router,
        Method::GET,
        "/api/auth/api-key/list",
        serde_json::Value::Null,
        Some(&user.cookie),
        None,
    )
    .await?;
    assert_eq!(populated.status, StatusCode::OK);
    assert_eq!(populated.body["total"], 1);

    adapter
        .delete(
            Delete::new(API_KEY_MODEL)
                .where_clause(Where::new("id", DbValue::String(key_id.to_owned()))),
        )
        .await?;

    let cached = request_json(
        &router,
        Method::GET,
        "/api/auth/api-key/list",
        serde_json::Value::Null,
        Some(&user.cookie),
        None,
    )
    .await?;
    assert_eq!(cached.status, StatusCode::OK);
    assert_eq!(cached.body["total"], 1);
    assert_eq!(cached.body["apiKeys"][0]["id"], key_id);
    Ok(())
}

#[tokio::test]
async fn secondary_storage_list_fetches_key_records_concurrently(
) -> Result<(), Box<dyn std::error::Error>> {
    let adapter = Arc::new(MemoryAdapter::new());
    let storage = Arc::new(TestSecondaryStorage::with_get_delay(20));
    let router = test_router(
        adapter,
        api_key_with_options(ApiKeyOptions {
            configuration: ApiKeyConfiguration {
                storage: ApiKeyStorageMode::SecondaryStorage,
                custom_storage: Some(storage.clone()),
                ..ApiKeyConfiguration::default()
            },
        }),
    )?;
    let user = sign_up(&router, "Lux", "lux-api@example.com").await?;

    for index in 0..12 {
        let created = request_json(
            &router,
            Method::POST,
            "/api/auth/api-key/create",
            json!({"name": format!("key-{index:02}")}),
            Some(&user.cookie),
            None,
        )
        .await?;
        assert_eq!(created.status, StatusCode::OK);
    }

    let listed = request_json(
        &router,
        Method::GET,
        "/api/auth/api-key/list",
        serde_json::Value::Null,
        Some(&user.cookie),
        None,
    )
    .await?;
    assert_eq!(listed.status, StatusCode::OK);
    assert_eq!(listed.body["total"], 12);
    assert!(
        storage.max_active_gets() > 1,
        "expected list to fetch multiple API key records concurrently"
    );
    Ok(())
}