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 backup-code contracts must fail immediately on invalid fixture shapes or native method observations."
)]

use super::*;
use crate::plugins::test_helpers::verification_consumer_native::{
    Harness, NOW, ReplayFailures, SECRET, assert_replay, date, object::Root,
};
use better_auth_core::{
    AuthPlugin, FieldFunction, NativeRequest,
    store::{AuthStore, StatelessSchema, schema::EntityRole},
};
use serde_json::{Value as Json, json};
use std::sync::{OnceLock, Weak};

const IDENTIFIER: &str = "2fa-backup-native";
const CIPHERTEXT: &str = "native-backup-ciphertext";

fn factor() -> FieldMap {
    FieldMap::from([
        ("id".into(), "factor-row".into()),
        ("secret".into(), "native-factor-secret".into()),
        ("backupCodes".into(), CIPHERTEXT.into()),
        ("userId".into(), "owner".into()),
        ("verified".into(), true.into()),
        ("failedVerificationCount".into(), 2.into()),
        ("lockedUntil".into(), FieldValue::Null),
    ])
}

fn owner(owner: &Weak<OnceLock<FieldValue>>) -> FieldValue {
    owner.upgrade().unwrap().get().unwrap().clone()
}

fn visit(visits: &FieldValue, name: String) -> AuthResult<()> {
    let index = visits.as_array().unwrap().len()?;
    visits.set_model_property(&index.to_string(), name.into())
}

fn method(
    object: &Root,
    name: &'static str,
    harness: &Arc<Harness>,
    visits: &FieldValue,
    body: impl Fn(&[FieldValue]) -> AuthResult<FieldValue> + Send + Sync + 'static,
) {
    let expected = object.owner();
    let harness = harness.clone();
    let visits = visits.clone();
    let body = Arc::new(body);
    object.getter(name, false, move |receiver| {
        visit(&visits, format!("{name}:get"))?;
        harness.record(
            &format!("get:{name}"),
            [(
                "sameReceiver".into(),
                receiver.strict_equals(&owner(&expected)).into(),
            )],
        );
        if harness.scenario["fail"] == format!("{name}:get") {
            return harness.fail(&format!("throw:{name}:get"));
        }
        let expected = expected.clone();
        let harness = harness.clone();
        let visits = visits.clone();
        let body = body.clone();
        Ok(FieldFunction::contextual(move |receiver, arguments| {
            visit(&visits, format!("{name}:call"))?;
            harness.record(
                &format!("call:{name}"),
                [
                    (
                        "sameReceiver".into(),
                        receiver.strict_equals(&owner(&expected)).into(),
                    ),
                    (
                        "arguments".into(),
                        harness.observe(&arguments.to_vec().into())?,
                    ),
                ],
            );
            assert!(receiver.strict_equals(&owner(&expected)));
            assert_eq!(arguments.len(), 1);
            if harness.scenario["fail"] == format!("{name}:call") {
                return harness.fail(&format!("throw:{name}:call"));
            }
            body(arguments)
        })
        .into())
    });
}

struct Methods {
    codes: Root,
    visits: FieldValue,
    status: FieldValue,
    updated: FieldValue,
    _updated_root: Option<Root>,
}

impl Methods {
    fn new(harness: &Arc<Harness>) -> AuthResult<Self> {
        let codes = Root::new("date");
        let visits: FieldValue = Vec::<FieldValue>::new().into();
        codes.data("marker", "native-codes".into());
        codes.data("visits", visits.clone());
        let status: FieldValue = match harness.scenario["status"].as_str().unwrap() {
            "false" => false.into(),
            "object" => FieldMap::from([("marker".into(), "native-status".into())]).into(),
            "true" => true.into(),
            value => {
                return Err(AuthError::internal(format!(
                    "Unknown backup status: {value}"
                )));
            }
        };
        let updated_kind = harness.scenario["updated"].as_str().unwrap();
        let updated_root = updated_kind.starts_with("object").then(|| {
            let root = Root::new("object");
            root.data("marker", "native-filter-result".into());
            root.data("ownUndefined", FieldValue::Undefined);
            let json_undefined = updated_kind == "object-undefined";
            method(&root, "toJSON", harness, &visits, move |arguments| {
                assert_eq!(arguments, &[FieldValue::from("")]);
                if json_undefined {
                    return Ok(FieldValue::Undefined);
                }
                Ok(FieldMap::from([
                    ("codes".into(), vec![FieldValue::from("kept")].into()),
                    ("tag".into(), "native".into()),
                ])
                .into())
            });
            root
        });
        let updated = match harness.scenario["updated"].as_str().unwrap() {
            "undefined" => FieldValue::Undefined,
            "null" => FieldValue::Null,
            "false" => false.into(),
            "zero" => 0.into(),
            "nan" => f64::NAN.into(),
            "empty" => "".into(),
            "string" => "kept".into(),
            "function" => FieldFunction::contextual(|_, _| Ok(FieldValue::Undefined)).into(),
            "object" | "object-undefined" => updated_root.as_ref().unwrap().value.clone(),
            "array" => vec![FieldValue::from("keep"), 7.into()].into(),
            value => {
                return Err(AuthError::internal(format!(
                    "Unknown backup result: {value}"
                )));
            }
        };
        let returned_status = status.clone();
        method(&codes, "includes", harness, &visits, move |arguments| {
            assert_eq!(arguments, &[FieldValue::from("used")]);
            Ok(returned_status.clone())
        });
        let returned_updated = updated.clone();
        let expected = codes.owner();
        let observed = harness.clone();
        method(&codes, "filter", harness, &visits, move |arguments| {
            let [FieldValue::Function(predicate)] = arguments else {
                return Err(AuthError::internal(
                    "Filter requires the original predicate",
                ));
            };
            let codes = owner(&expected);
            let results = vec![
                predicate.call_with(
                    &"ignored receiver".into(),
                    &["used".into(), 9.into(), codes.clone()],
                )?,
                predicate.call_with(&codes, &[vec![FieldValue::from("used")].into()])?,
                predicate.call()?,
            ];
            assert_eq!(results, [false.into(), true.into(), true.into()]);
            observed.record(
                "filter:predicate",
                [("results".into(), observed.observe(&results.into())?)],
            );
            Ok(returned_updated.clone())
        });
        Ok(Self {
            codes,
            visits,
            status,
            updated,
            _updated_root: updated_root,
        })
    }

    fn state(&self, harness: &Harness) -> AuthResult<Json> {
        harness.observe(
            &FieldMap::from([
                ("root".into(), self.codes.value.clone()),
                ("visits".into(), self.visits.clone()),
                ("status".into(), self.status.clone()),
                ("updated".into(), self.updated.clone()),
            ])
            .into(),
        )
    }
}

struct Cipher {
    harness: Arc<Harness>,
    codes: FieldValue,
    database: OnceLock<Arc<dyn AuthStore<StatelessSchema>>>,
}

#[async_trait]
impl TwoFactorCipher for Cipher {
    async fn encrypt(&self, _: &str) -> AuthResult<String> {
        Err(AuthError::internal(
            "The native contract must not narrow the cipher input",
        ))
    }

    async fn encrypt_native(&self, plaintext: &FieldValue) -> AuthResult<FieldValue> {
        self.harness.record(
            "cipher:encrypt",
            [("value".into(), self.harness.observe(plaintext)?)],
        );
        if self.harness.scenario["fail"] == "cipher:encrypt" {
            return self.harness.fail("throw:cipher:encrypt");
        }
        if self.harness.scenario["encrypted"] == "string" {
            return Ok("encoded-native-backup-codes".into());
        }
        Ok(plaintext.clone())
    }

    async fn decrypt(&self, _: &str) -> AuthResult<String> {
        Err(AuthError::internal(
            "The native contract must not narrow the cipher result",
        ))
    }

    async fn decrypt_native(&self, ciphertext: &FieldValue) -> AuthResult<FieldValue> {
        self.harness.record(
            "cipher:decrypt",
            [
                (
                    "sameCiphertext".into(),
                    ciphertext.strict_equals(&CIPHERTEXT.into()).into(),
                ),
                ("value".into(), self.harness.observe(ciphertext)?),
            ],
        );
        assert_eq!(ciphertext, &FieldValue::from(CIPHERTEXT));
        if self.harness.scenario["replace"] == true {
            let replacement = self
                .database
                .get()
                .unwrap()
                .update_two_factor_record(
                    &"factor-row".to_owned().into(),
                    FieldMap::from([("backupCodes".into(), "concurrent-backup-ciphertext".into())]),
                )
                .await?;
            let replacement = replacement.map_or(FieldValue::Null, Into::into);
            self.harness.record(
                "cipher:replace",
                [("result".into(), self.harness.observe(&replacement)?)],
            );
        }
        Ok(self.codes.clone())
    }
}

pub(super) async fn capture(scenario: &Json) -> AuthResult<Json> {
    let harness = Arc::new(Harness::new(scenario, IDENTIFIER)?.with_model_rows(
        "twoFactor",
        EntityRole::TwoFactor,
        vec![factor()],
    ));
    let attempts_identifier = format!("2fa-attempts-{IDENTIFIER}");
    let attempts = FieldValue::from(FieldMap::from([
        ("id".into(), "attempts-row".into()),
        ("identifier".into(), attempts_identifier.clone().into()),
        ("value".into(), "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:{attempts_identifier}"),
        attempts.stringify()?.unwrap().into(),
    )?;
    let methods = Methods::new(&harness)?;
    let codes_before = methods.state(&harness)?;
    let cipher = Arc::new(Cipher {
        harness: harness.clone(),
        codes: methods.codes.value.clone(),
        database: OnceLock::new(),
    });
    let plugin = TwoFactorPlugin::with_config(TwoFactorConfig {
        backup_code_options: BackupCodeOptions {
            storage: BackupCodeStorage::Custom(cipher.clone()),
            ..Default::default()
        },
        ..Default::default()
    });
    let (context, inner) = harness.context(&[&plugin]).await?;
    assert!(cipher.database.set(context.database.clone()).is_ok());
    let route = <TwoFactorPlugin as AuthPlugin<StatelessSchema>>::routes(&plugin)
        .into_iter()
        .find(|route| route.path == "/two-factor/verify-backup-code")
        .unwrap();
    let cookie_name = related_cookie_name(&context.config, TWO_FACTOR_COOKIE_SUFFIX);
    let signed = sign_cookie_value(SECRET, IDENTIFIER)?;
    let headers =
        std::collections::HashMap::from([("cookie".into(), format!("{cookie_name}={signed}"))]);
    let before = harness.state(&inner)?;
    harness.activate();
    let result =
        context
            .dispatch_native(
                NativeRequest {
                    headers: Some(&headers),
                    ..Default::default()
                },
                route,
                Some(json!({"code":"used","disableSession":true})),
                None,
                |request, context| async move {
                    plugin.handle_verify_backup_code(&request, &context).await
                },
            )
            .await
            .and_then(|response| response.body.field_value());
    let mut captured = harness.finish(before, harness.outcome(result)?, &inner)?;
    captured["codes"] = json!({"before":codes_before,"after":methods.state(&harness)?});
    Ok(captured)
}

#[tokio::test]
async fn backup_code_methods_match_upstream_runtime_and_failure_persistence() -> AuthResult<()> {
    let fixture: Json = serde_json::from_str(
        &std::fs::read_to_string(
            std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
                .join("../../tests/fixtures/two-factor-backup-code-native-1.7.6.json"),
        )
        .unwrap(),
    )?;
    assert_eq!(fixture["version"], "1.7.6");
    assert_eq!(fixture["contract"], "two-factor-backup-code-native");
    let cases = fixture["cases"].as_array().unwrap();
    assert_eq!(cases.len(), 16);
    let mut failures = ReplayFailures::default();
    for case in cases {
        let name = case["name"].as_str().unwrap();
        let actual = capture(&case["scenario"]).await?;
        failures.record(name, || assert_replay(&actual, &case["runtime"], name));
    }
    failures.finish();
    Ok(())
}