rustauth-plugins 0.2.0

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

use http::{Method, StatusCode};
use rustauth_core::db::{DbAdapter, DbValue, FindOne, MemoryAdapter, Update, Where};
use rustauth_plugins::api_key::{
    api_key, ApiKeyConfiguration, ApiKeyOptions, API_KEY_MODEL, METADATA_DISABLED,
    NO_VALUES_TO_UPDATE,
};
use serde_json::json;

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

#[tokio::test]
async fn metadata_requires_explicit_enablement() -> 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, "Fay", "fay-api@example.com").await?;

    let created = request_json(
        &router,
        Method::POST,
        "/api/auth/api-key/create",
        json!({"name":"plain"}),
        Some(&user.cookie),
        None,
    )
    .await?;
    let key_id = created.body["id"].as_str().ok_or("missing api key id")?;

    let create_with_metadata = request_json(
        &router,
        Method::POST,
        "/api/auth/api-key/create",
        json!({"name":"with-metadata","metadata":{"env":"prod"}}),
        Some(&user.cookie),
        None,
    )
    .await?;
    assert_eq!(create_with_metadata.status, StatusCode::BAD_REQUEST);
    assert_eq!(create_with_metadata.body["code"], METADATA_DISABLED);

    let update_with_metadata = request_json(
        &router,
        Method::POST,
        "/api/auth/api-key/update",
        json!({"keyId": key_id, "metadata":{"env":"prod"}}),
        Some(&user.cookie),
        None,
    )
    .await?;
    assert_eq!(update_with_metadata.status, StatusCode::BAD_REQUEST);
    assert_eq!(update_with_metadata.body["code"], NO_VALUES_TO_UPDATE);
    Ok(())
}

#[tokio::test]
async fn database_backed_reads_migrate_double_stringified_metadata(
) -> Result<(), Box<dyn std::error::Error>> {
    let adapter = Arc::new(MemoryAdapter::new());
    let router = test_router(
        adapter.clone(),
        api_key(
            ApiKeyOptions::builder()
                .configuration(ApiKeyConfiguration {
                    enable_metadata: true,
                    ..ApiKeyConfiguration::default()
                })
                .build()?,
        )?,
    )?;
    let user = sign_up(&router, "Meta", "meta-api@example.com").await?;
    let mut keys = Vec::new();
    for name in ["get", "list", "verify", "update"] {
        let created = request_json(
            &router,
            Method::POST,
            "/api/auth/api-key/create",
            json!({"name": name, "metadata": {"initial": name}}),
            Some(&user.cookie),
            None,
        )
        .await?;
        keys.push((
            created.body["id"].as_str().ok_or("missing id")?.to_owned(),
            created.body["key"]
                .as_str()
                .ok_or("missing key")?
                .to_owned(),
        ));
    }

    for (index, (id, _key)) in keys.iter().enumerate() {
        set_legacy_metadata(&adapter, id, json!({ "legacy": index })).await?;
    }

    let get = request_json(
        &router,
        Method::GET,
        &format!("/api/auth/api-key/get?id={}", keys[0].0),
        serde_json::Value::Null,
        Some(&user.cookie),
        None,
    )
    .await?;
    assert_eq!(get.body["metadata"], json!({ "legacy": 0 }));
    assert_metadata_migrated(&adapter, &keys[0].0, json!({ "legacy": 0 })).await?;

    let list = request_json(
        &router,
        Method::GET,
        "/api/auth/api-key/list",
        serde_json::Value::Null,
        Some(&user.cookie),
        None,
    )
    .await?;
    assert!(
        list.body["apiKeys"]
            .as_array()
            .unwrap_or(&Vec::new())
            .iter()
            .any(|api_key| api_key["id"] == keys[1].0
                && api_key["metadata"] == json!({ "legacy": 1 }))
    );
    assert_metadata_migrated(&adapter, &keys[1].0, json!({ "legacy": 1 })).await?;

    let verify = request_json(
        &router,
        Method::POST,
        "/api/auth/api-key/verify",
        json!({"key": keys[2].1}),
        None,
        None,
    )
    .await?;
    assert_eq!(verify.body["key"]["metadata"], json!({ "legacy": 2 }));
    assert_metadata_migrated(&adapter, &keys[2].0, json!({ "legacy": 2 })).await?;

    let update = request_json(
        &router,
        Method::POST,
        "/api/auth/api-key/update",
        json!({"keyId": keys[3].0, "name": "updated"}),
        Some(&user.cookie),
        None,
    )
    .await?;
    assert_eq!(update.body["metadata"], json!({ "legacy": 3 }));
    assert_metadata_migrated(&adapter, &keys[3].0, json!({ "legacy": 3 })).await?;
    Ok(())
}

async fn set_legacy_metadata(
    adapter: &MemoryAdapter,
    id: &str,
    metadata: serde_json::Value,
) -> Result<(), Box<dyn std::error::Error>> {
    let legacy = serde_json::to_string(&serde_json::to_string(&metadata)?)?;
    adapter
        .update(
            Update::new(API_KEY_MODEL)
                .where_clause(Where::new("id", DbValue::String(id.to_owned())))
                .data("metadata", DbValue::String(legacy)),
        )
        .await?;
    Ok(())
}

#[tokio::test]
async fn properly_formatted_metadata_does_not_require_migration(
) -> Result<(), Box<dyn std::error::Error>> {
    let adapter = Arc::new(MemoryAdapter::new());
    let router = test_router(
        adapter.clone(),
        api_key(
            ApiKeyOptions::builder()
                .configuration(ApiKeyConfiguration {
                    enable_metadata: true,
                    ..ApiKeyConfiguration::default()
                })
                .build()?,
        )?,
    )?;
    let user = sign_up(&router, "Plain", "plain-meta@example.com").await?;
    let metadata = json!({ "alreadyCorrect": true, "value": 123 });

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

    let fetched = request_json(
        &router,
        Method::GET,
        &format!("/api/auth/api-key/get?id={key_id}"),
        serde_json::Value::Null,
        Some(&user.cookie),
        None,
    )
    .await?;
    assert_eq!(fetched.body["metadata"], metadata);
    assert_metadata_migrated(&adapter, key_id, metadata).await?;
    Ok(())
}

async fn assert_metadata_migrated(
    adapter: &MemoryAdapter,
    id: &str,
    expected: serde_json::Value,
) -> Result<(), Box<dyn std::error::Error>> {
    let record = adapter
        .find_one(
            FindOne::new(API_KEY_MODEL)
                .where_clause(Where::new("id", DbValue::String(id.to_owned()))),
        )
        .await?
        .ok_or("missing api key")?;
    assert_eq!(record.get("metadata"), Some(&DbValue::Json(expected)));
    Ok(())
}