rustauth-plugins 0.2.0

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

use http::{Method, StatusCode};
use rustauth_core::db::MemoryAdapter;
use rustauth_plugins::api_key::{
    api_key, ApiKeyConfiguration, ApiKeyOptions, ApiKeyRateLimitOptions, ApiKeyReference,
    UPSTREAM_PLUGIN_ID,
};
use serde_json::{json, Value};

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

#[test]
fn multiple_configurations_require_unique_config_ids() -> Result<(), Box<dyn std::error::Error>> {
    let missing = ApiKeyOptions::builder().configurations(vec![
        ApiKeyConfiguration::default(),
        ApiKeyConfiguration {
            config_id: Some("second".to_owned()),
            ..ApiKeyConfiguration::default()
        },
    ]);
    assert!(missing.build().is_err());

    let duplicate = ApiKeyOptions::builder().configurations(vec![
        ApiKeyConfiguration {
            config_id: Some("default".to_owned()),
            ..ApiKeyConfiguration::default()
        },
        ApiKeyConfiguration {
            config_id: Some("default".to_owned()),
            ..ApiKeyConfiguration::default()
        },
    ]);
    assert!(duplicate.build().is_err());

    let plugin = api_key(
        ApiKeyOptions::builder()
            .configurations(vec![
                ApiKeyConfiguration {
                    config_id: Some("user-keys".to_owned()),
                    reference: ApiKeyReference::User,
                    ..ApiKeyConfiguration::default()
                },
                ApiKeyConfiguration {
                    config_id: Some("org-keys".to_owned()),
                    reference: ApiKeyReference::Organization,
                    ..ApiKeyConfiguration::default()
                },
            ])
            .build()?,
    )?;
    assert_eq!(plugin.id, UPSTREAM_PLUGIN_ID);

    Ok(())
}

#[tokio::test]
async fn list_without_config_id_merges_user_keys_from_all_configurations(
) -> Result<(), Box<dyn std::error::Error>> {
    let adapter = Arc::new(MemoryAdapter::new());
    let plugin = api_key(
        ApiKeyOptions::builder()
            .configurations(vec![
                ApiKeyConfiguration {
                    config_id: Some("primary".to_owned()),
                    ..ApiKeyConfiguration::default()
                },
                ApiKeyConfiguration {
                    config_id: Some("secondary".to_owned()),
                    ..ApiKeyConfiguration::default()
                },
            ])
            .build()?,
    )?;
    let router = test_router(adapter, plugin)?;
    let user = sign_up(&router, "Jay", "jay-api@example.com").await?;

    for (config_id, name) in [("primary", "one"), ("secondary", "two")] {
        let created = request_json(
            &router,
            Method::POST,
            "/api/auth/api-key/create",
            json!({"configId": config_id, "name": name}),
            Some(&user.cookie),
            None,
        )
        .await?;
        assert_eq!(created.status, StatusCode::OK);
    }

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

#[tokio::test]
async fn specific_config_id_controls_create_verify_get_update_and_delete(
) -> Result<(), Box<dyn std::error::Error>> {
    let adapter = Arc::new(MemoryAdapter::new());
    let plugin = api_key(
        ApiKeyOptions::builder()
            .configurations(vec![
                ApiKeyConfiguration {
                    config_id: Some("default".to_owned()),
                    default_prefix: Some("def_".to_owned()),
                    rate_limit: ApiKeyRateLimitOptions {
                        max_requests: 10,
                        ..ApiKeyRateLimitOptions::default()
                    },
                    ..ApiKeyConfiguration::default()
                },
                ApiKeyConfiguration {
                    config_id: Some("public-api".to_owned()),
                    default_prefix: Some("pub_".to_owned()),
                    rate_limit: ApiKeyRateLimitOptions {
                        max_requests: 15,
                        ..ApiKeyRateLimitOptions::default()
                    },
                    ..ApiKeyConfiguration::default()
                },
            ])
            .build()?,
    )?;
    let router = test_router(adapter, plugin)?;
    let user = sign_up(&router, "Cfg", "cfg-api@example.com").await?;

    let created = request_json(
        &router,
        Method::POST,
        "/api/auth/api-key/create",
        json!({"configId":"public-api","name":"public-key"}),
        Some(&user.cookie),
        None,
    )
    .await?;
    assert_eq!(created.status, StatusCode::OK);
    assert_eq!(created.body["configId"], "public-api");
    assert_eq!(created.body["prefix"], "pub_");
    assert_eq!(created.body["rateLimitMax"], 15);
    let key = created.body["key"].as_str().ok_or("missing key")?;
    let key_id = created.body["id"].as_str().ok_or("missing id")?;

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

    let listed = request_json(
        &router,
        Method::GET,
        "/api/auth/api-key/list?configId=public-api",
        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]["id"], key_id);

    let fetched = request_json(
        &router,
        Method::GET,
        &format!("/api/auth/api-key/get?configId=public-api&id={key_id}"),
        Value::Null,
        Some(&user.cookie),
        None,
    )
    .await?;
    assert_eq!(fetched.status, StatusCode::OK);
    assert_eq!(fetched.body["configId"], "public-api");

    let updated = request_json(
        &router,
        Method::POST,
        "/api/auth/api-key/update",
        json!({"configId":"public-api","keyId": key_id, "name":"updated-public"}),
        Some(&user.cookie),
        None,
    )
    .await?;
    assert_eq!(updated.status, StatusCode::OK);
    assert_eq!(updated.body["configId"], "public-api");
    assert_eq!(updated.body["name"], "updated-public");

    let deleted = request_json(
        &router,
        Method::POST,
        "/api/auth/api-key/delete",
        json!({"configId":"public-api","keyId": key_id}),
        Some(&user.cookie),
        None,
    )
    .await?;
    assert_eq!(deleted.status, StatusCode::OK);
    assert_eq!(deleted.body["success"], true);
    Ok(())
}

#[tokio::test]
async fn list_combines_sorting_with_pagination() -> Result<(), Box<dyn std::error::Error>> {
    let adapter = Arc::new(MemoryAdapter::new());
    let router = test_router(adapter, api_key(ApiKeyOptions::default())?)?;
    let user = sign_up(&router, "Pag", "pag-api@example.com").await?;

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

    let listed = request_json(
        &router,
        Method::GET,
        "/api/auth/api-key/list?limit=3&offset=1&sortBy=name&sortDirection=desc",
        Value::Null,
        Some(&user.cookie),
        None,
    )
    .await?;
    assert_eq!(listed.status, StatusCode::OK);
    assert_eq!(listed.body["limit"], 3);
    assert_eq!(listed.body["offset"], 1);
    assert_eq!(listed.body["total"], 5);
    let keys = listed.body["apiKeys"].as_array().ok_or("missing apiKeys")?;
    assert_eq!(keys.len(), 3);
    assert_eq!(keys[0]["name"], "pag-key-3");
    assert_eq!(keys[1]["name"], "pag-key-2");
    assert_eq!(keys[2]["name"], "pag-key-1");
    Ok(())
}