rustauth-core 0.2.0

Core types and primitives for RustAuth.
Documentation
use http::{Method, Request, StatusCode};
use rustauth_core::api::{response, ApiRequest, ApiResponse, AuthEndpoint, AuthRouter};
use rustauth_core::context::{create_auth_context_with_environment, AuthEnvironment};
use rustauth_core::cookies::get_cookies;
use rustauth_core::env::{allows_development_defaults, is_production_posture};
use rustauth_core::error::RustAuthError;
use rustauth_core::options::RustAuthOptions;
use rustauth_core::rate_limit::resolve_client_ip;
use std::sync::{Mutex, MutexGuard, OnceLock};

struct EnvRestore(Vec<(&'static str, Option<String>)>);

impl EnvRestore {
    fn unset(keys: &[&'static str]) -> Self {
        let saved = keys
            .iter()
            .map(|key| (*key, std::env::var(key).ok()))
            .collect::<Vec<_>>();
        for key in keys {
            std::env::remove_var(key);
        }
        Self(saved)
    }
}

impl Drop for EnvRestore {
    fn drop(&mut self) {
        for (key, value) in &self.0 {
            match value {
                Some(value) => std::env::set_var(key, value),
                None => std::env::remove_var(key),
            }
        }
    }
}

fn env_lock() -> &'static Mutex<()> {
    static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
    LOCK.get_or_init(|| Mutex::new(()))
}

fn lock_ambiguous_env() -> (MutexGuard<'static, ()>, EnvRestore) {
    let guard = env_lock()
        .lock()
        .unwrap_or_else(|poisoned| poisoned.into_inner());
    let restore = EnvRestore::unset(&["RUST_ENV", "RUST_TEST_THREADS", "TEST", "NEXTEST"]);
    (guard, restore)
}

fn ambiguous_options() -> RustAuthOptions {
    RustAuthOptions::default()
}

#[test]
fn ambiguous_deployment_fails_closed_for_production_posture() {
    let _guard = lock_ambiguous_env();
    let options = ambiguous_options();

    assert!(is_production_posture(&options));
    assert!(!allows_development_defaults(&options));
}

#[test]
fn explicit_development_allows_development_defaults() {
    let _guard = lock_ambiguous_env();
    let options = RustAuthOptions::default().development(true);

    assert!(!is_production_posture(&options));
    assert!(allows_development_defaults(&options));
}

#[test]
fn ambiguous_deployment_rejects_default_secret() {
    let _guard = lock_ambiguous_env();
    let result =
        create_auth_context_with_environment(ambiguous_options(), AuthEnvironment::default());

    assert!(matches!(
        result,
        Err(RustAuthError::InvalidConfig(message))
            if message.contains("default secret cannot be used in production")
    ));
}

#[test]
fn ambiguous_deployment_enables_rate_limiting_by_default() -> Result<(), Box<dyn std::error::Error>>
{
    let _guard = lock_ambiguous_env();
    let context = create_auth_context_with_environment(
        RustAuthOptions {
            secret: Some("secret-a-at-least-32-chars-long!!".to_owned()),
            ..ambiguous_options()
        },
        AuthEnvironment::default(),
    )?;

    assert!(context.rate_limit.enabled);
    Ok(())
}

#[test]
fn ambiguous_deployment_does_not_fallback_to_localhost_ip() -> Result<(), Box<dyn std::error::Error>>
{
    let _guard = lock_ambiguous_env();
    let context = create_auth_context_with_environment(
        RustAuthOptions {
            secret: Some("secret-a-at-least-32-chars-long!!".to_owned()),
            ..ambiguous_options()
        },
        AuthEnvironment::default(),
    )?;
    let request = Request::builder()
        .method(Method::GET)
        .uri("http://localhost:3000/api/auth/ok")
        .body(Vec::new())?;

    assert_eq!(resolve_client_ip(&context, &request), None);
    Ok(())
}

#[test]
fn ambiguous_deployment_defaults_cookies_to_secure_without_base_url(
) -> Result<(), Box<dyn std::error::Error>> {
    let _guard = lock_ambiguous_env();
    let cookies = get_cookies(&ambiguous_options())?;

    assert_eq!(cookies.session_token.attributes.secure, Some(true));
    Ok(())
}

#[test]
fn ambiguous_deployment_denies_rate_limited_requests_without_client_ip(
) -> Result<(), Box<dyn std::error::Error>> {
    let _guard = lock_ambiguous_env();
    let context = create_auth_context_with_environment(
        RustAuthOptions {
            secret: Some("secret-a-at-least-32-chars-long!!".to_owned()),
            ..ambiguous_options()
        },
        AuthEnvironment::default(),
    )?;
    let router = AuthRouter::new(
        context,
        vec![AuthEndpoint {
            path: "/ok".to_owned(),
            method: Method::GET,
            handler: ok_handler,
        }],
    );

    let response = router.handle(
        Request::builder()
            .method(Method::GET)
            .uri("http://localhost:3000/api/auth/ok")
            .body(Vec::new())?,
    )?;

    assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);
    Ok(())
}

fn ok_handler(
    _context: &rustauth_core::context::AuthContext,
    _request: ApiRequest,
) -> Result<ApiResponse, RustAuthError> {
    response(StatusCode::OK, b"OK".to_vec())
}