better-auth-api 1.0.0-alpha.5

Plugin implementations for better-auth
#![expect(
    clippy::unwrap_used,
    clippy::indexing_slicing,
    clippy::panic_in_result_fn,
    reason = "Pinned Auth contracts must fail immediately on invalid source fixtures or business observations."
)]

use super::*;
use crate::plugins::{
    OAuthProxyConfig, OAuthProxyPlugin,
    password_management::PasswordManagementPlugin,
    test_helpers::verification_consumer_native::{
        Harness, NOW, ORIGIN, ReplayFailures, SECRET, assert_replay, consumer_user, date,
    },
};
use better_auth_core::{AuthPlugin, AuthRoute, Headers, HttpMethod, store::StatelessSchema};
use serde_json::{Value as Json, json};

mod trust;

fn headers(headers: &Headers) -> Json {
    let mut pairs = headers
        .iter()
        .map(|(name, value)| (name.to_ascii_lowercase(), value.clone()))
        .collect::<Vec<_>>();
    pairs.sort_by(|left, right| left.0.cmp(&right.0));
    json!(pairs)
}

fn cookies(headers: &Headers) -> Json {
    json!(headers.get_all("set-cookie").collect::<Vec<_>>())
}

fn request_cookie(
    request: &mut AuthRequest,
    context: &AuthContext<StatelessSchema>,
    suffix: &str,
    value: &str,
) -> AuthResult<()> {
    let name = related_cookie_name(&context.config, suffix);
    let signed = sign_cookie_value(SECRET, value)?;
    let _ = request
        .headers
        .insert("cookie".to_owned(), format!("{name}={signed}"));
    Ok(())
}

async fn password(scenario: &Json) -> AuthResult<Json> {
    let harness = Harness::new(scenario, "reset-password:native-token")?;
    let plugin = PasswordManagementPlugin::new();
    let (context, inner) = harness.context(&[&plugin]).await?;
    let mut request = AuthRequest::new(HttpMethod::Get, "/reset-password/native-token");
    request.query = Some(json!({"callbackURL":format!("{ORIGIN}/reset?kept=1")}));
    let before = harness.state(&inner)?;
    harness.activate();
    let harness = &harness;
    let request_ref = &request;
    let result = context
        .with_http_context(&request, |context| async move {
            let response = match plugin.on_request(request_ref, &context).await {
                Ok(response) => response.unwrap().into_http_response()?,
                Err(error) => {
                    better_auth_core::api_error::handle_http_error(error, &context).await?
                }
            };
            FieldValue::from_json(json!({
                "status":response.status,
                "headers":headers(&response.headers),
                "cookies":cookies(&response.headers),
                "body":String::from_utf8(response.body.into_bytes()?).unwrap(),
            }))
        })
        .await;
    harness.finish(before, harness.outcome(result)?, &inner)
}

fn resolved(state: &ResolvedTwoFactorState) -> AuthResult<FieldValue> {
    let session = match state {
        ResolvedTwoFactorState::Session { data, .. } => data.session_value()?,
        ResolvedTwoFactorState::Pending(_) => FieldValue::Null,
    };
    Ok(FieldMap::from([
        (
            "session".into(),
            FieldMap::from([("session".into(), session), ("user".into(), state.user()?)]).into(),
        ),
        ("key".into(), state.key().into()),
    ])
    .into())
}

async fn two_factor(scenario: &Json) -> AuthResult<Json> {
    let identifier = "2fa-native";
    let harness = Harness::new(scenario, identifier)?;
    let rearm = scenario["consumer"] == "two-factor-rearm";
    if rearm {
        let attempts = FieldValue::from(FieldMap::from([
            ("id".into(), "attempts-row".into()),
            (
                "identifier".into(),
                format!("2fa-attempts-{identifier}").into(),
            ),
            (
                "value".into(),
                if scenario["exhausted"] == true {
                    "3"
                } else {
                    "0"
                }
                .into(),
            ),
            ("expiresAt".into(), date((NOW + 60_000) as f64)),
            ("createdAt".into(), date(NOW as f64)),
            ("updatedAt".into(), date(NOW as f64)),
        ]));
        harness.cache.insert(
            &format!("verification:2fa-attempts-{identifier}"),
            attempts.stringify()?.unwrap().into(),
        )?;
    }
    let plugin = TwoFactorPlugin::new();
    let (context, inner) = harness.context(&[&plugin]).await?;
    let mut request = AuthRequest::new(HttpMethod::Post, "/native-verification-consumer");
    request_cookie(&mut request, &context, TWO_FACTOR_COOKIE_SUFFIX, identifier)?;
    let before = harness.state(&inner)?;
    harness.activate();
    let harness = &harness;
    let result = context
        .dispatch_native(
            better_auth_core::NativeRequest {
                headers: Some(&request.headers),
                ..Default::default()
            },
            AuthRoute::server_only(HttpMethod::Post, "captureNativeVerification"),
            Some(json!({})),
            None,
            |request, context| async move {
                let result: AuthResult<FieldValue> = async {
                    let state = resolve_two_factor_state(&request, &context).await?;
                    harness.record("consumer:resolved", [("key".into(), state.key().into())]);
                    if !rearm {
                        return resolved(&state);
                    }
                    let attempt = begin_attempt(&state, &request, &context, 3).await?;
                    harness.record("consumer:attempt-consumed", []);
                    let result =
                        finish_attempt(attempt, scenario["restore"] != true, &context).await?;
                    harness.record(
                        "consumer:rearmed",
                        [("result".into(), harness.observe(&result)?)],
                    );
                    Ok(FieldMap::from([
                        ("verification".into(), resolved(&state)?),
                        ("result".into(), result),
                    ])
                    .into())
                }
                .await;
                let accumulated = Headers::from_field_value(&request.response_headers_value()?)?;
                harness.record(
                    "consumer:two-factor-finally",
                    [
                        ("headers".into(), headers(&accumulated)),
                        ("cookies".into(), cookies(&accumulated)),
                    ],
                );
                result.map(|value| AuthResponse::native(None, value))
            },
        )
        .await
        .and_then(|response| response.body.field_value());
    harness.finish(before, harness.outcome(result)?, &inner)
}

async fn proxy(scenario: &Json) -> AuthResult<Json> {
    let harness = Harness::new(scenario, "native-oauth-state")?;
    let plugin = OAuthProxyPlugin::with_config(OAuthProxyConfig {
        production_url: Some("https://production.verification-consumer.test".into()),
        current_url: Some(ORIGIN.into()),
        ..Default::default()
    });
    let (context, inner) = harness.context(&[&plugin]).await?;
    assert_eq!(
        context.config.account.store_state_strategy(),
        better_auth_core::OAuthStateStrategy::Database
    );
    let mut request = AuthRequest::new(HttpMethod::Post, "/sign-in/social");
    request.body = Some(serde_json::to_vec(&json!({"provider":"fixture"}))?);
    let returned = FieldValue::from(FieldMap::from([
        (
            "url".into(),
            "https://provider.verification-consumer.test/authorize?state=native-oauth-state&kept=1"
                .into(),
        ),
        ("redirect".into(), true.into()),
    ]));
    let mut response = AuthResponse::native(None, returned.clone());
    let before = harness.state(&inner)?;
    harness.activate();
    let harness = &harness;
    let result = context
        .with_native_context(
            better_auth_core::NativeRequest::default(),
            |context| async move {
                plugin
                    .after_request(&request, &mut response, &context)
                    .await?;
                let response = response.body.field_value()?;
                Ok(FieldMap::from([
                    ("result".into(), FieldValue::Undefined),
                    ("response".into(), response.clone()),
                    (
                        "sameResponse".into(),
                        response.strict_equals(&returned).into(),
                    ),
                ])
                .into())
            },
        )
        .await;
    harness.finish(before, harness.outcome(result)?, &inner)
}

fn business_contract(case: &Json) -> Json {
    let mut expected = case["runtime"].clone();
    let consumer = case["scenario"]["consumer"].as_str().unwrap();
    if expected["outcome"]["returned"] == true
        && matches!(consumer, "two-factor-resolve" | "two-factor-rearm")
    {
        let result = expected["outcome"]["result"].as_object_mut().unwrap();
        let verification = if consumer == "two-factor-rearm" {
            let attempt = result.shift_remove("attempt").unwrap();
            assert_eq!(
                attempt
                    .as_object()
                    .unwrap()
                    .keys()
                    .map(String::as_str)
                    .collect::<Vec<_>>(),
                ["recordFailure", "restore"]
            );
            for function in attempt.as_object().unwrap().values() {
                assert_eq!(function["type"], "function");
            }
            result
                .get_mut("verification")
                .unwrap()
                .as_object_mut()
                .unwrap()
        } else {
            result
        };
        let mut unpaired = Vec::new();
        for name in ["valid", "invalid", "beginAttempt"] {
            let function = verification.shift_remove(name).unwrap();
            assert_eq!(function["type"], "function");
            unpaired.push(function["identity"].clone());
        }
        if consumer == "two-factor-rearm" {
            let attempt = &case["runtime"]["outcome"]["result"]["attempt"];
            unpaired.extend([
                attempt["recordFailure"]["identity"].clone(),
                attempt["restore"]["identity"].clone(),
            ]);
        }
        expected["functionIds"]
            .as_array_mut()
            .unwrap()
            .retain(|identity| !unpaired.contains(identity));
        eprintln!(
            "{}: returned async helper identities remain unpaired: {unpaired:?}",
            case["name"]
        );
    }
    if consumer == "two-factor-trust" {
        let event = expected["events"]
            .as_array_mut()
            .unwrap()
            .last_mut()
            .unwrap();
        assert_eq!(event["phase"], "consumer:trust-finally");
        assert_eq!(
            event
                .as_object_mut()
                .unwrap()
                .shift_remove("newSessionSameObject"),
            Some(true.into())
        );
        eprintln!(
            "{}: NativeSessionData outer object identity remains unpaired; retained User identity is asserted separately",
            case["name"]
        );
    }
    expected
}

#[tokio::test]
async fn auth_consumers_match_upstream_business_runtime_with_explicit_unpaired_host_boundaries()
-> AuthResult<()> {
    let fixture: Json = serde_json::from_str(
        &std::fs::read_to_string(
            std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
                .join("../../tests/fixtures/verification-auth-consumer-native-1.7.6.json"),
        )
        .unwrap(),
    )?;
    assert_eq!(fixture["version"], "1.7.6");
    let cases = fixture["cases"].as_array().unwrap();
    assert_eq!(cases.len(), 63);
    let mut paired = 0;
    let mut failures = ReplayFailures::default();
    for case in cases {
        let name = case["name"].as_str().unwrap();
        if name == "oauth-proxy-truthy-value-key-error-is-logged" {
            failures.record(name, || {
                assert_eq!(case["scenario"]["keyError"], true);
                eprintln!(
                    "{name}: native OAuth Proxy secret getter remains unpaired by Option<String>"
                );
            });
            continue;
        }
        let scenario = &case["scenario"];
        let actual = match scenario["consumer"].as_str().unwrap() {
            "password-redirect" => password(scenario).await?,
            "two-factor-resolve" | "two-factor-rearm" => two_factor(scenario).await?,
            "two-factor-trust" => trust::capture(scenario).await?,
            "oauth-proxy" => proxy(scenario).await?,
            consumer => {
                return Err(AuthError::internal(format!(
                    "Unknown Auth consumer: {consumer}"
                )));
            }
        };
        failures.record(name, || {
            assert_replay(&actual, &business_contract(case), name)
        });
        paired += 1;
    }
    assert_eq!(paired, 62);
    failures.finish();
    Ok(())
}