better-auth-api 1.0.0-alpha.5

Plugin implementations for better-auth
use super::apple_tests::{Mapper, TestResult, configured, fixture, rows, text};
use super::google_test_support::GoogleFixture;
use super::*;
use better_auth_seaorm::store::__private_test_support::bundled_schema::BundledSchema;
use serde_json::{Value, json};
use std::sync::mpsc;

#[tokio::test]
async fn apple_direct_and_form_post_success_persist_user_account_and_session() -> TestResult<()> {
    let fixture = fixture()?;
    for sample in rows(&fixture, "/flows")? {
        let mode = text(sample, "/mode")?;
        let server =
            GoogleFixture::start(sample.get("claims").ok_or("Missing claims")?.clone()).await;
        let mut provider = configured(&fixture, &json!({}), Some(&server))?;
        let (seen, captured) = mpsc::channel();
        let (calls, called) = mpsc::channel();
        provider.map_profile_to_user = Some(Arc::new(Mapper {
            patch: json!({"name":"Mapped Flow Owner"}),
            seen,
            calls,
        }));
        let plugin = OAuthPlugin::new().add_provider("apple", provider);
        let ctx = crate::plugins::test_helpers::create_test_context().await;
        let user_input = sample.get("userInput").ok_or("Missing callback user")?;
        let mut start = AuthRequest::new(HttpMethod::Post, "/sign-in/social");
        start.body = Some(serde_json::to_vec(&if mode == "direct" {
            json!({"provider":"apple", "idToken":{"token":server.token,"nonce":"ordinary-flow-nonce","user":user_input}})
        } else {
            json!({"provider":"apple","callbackURL":"http://localhost:3000/welcome","disableRedirect":true})
        })?);
        let response = plugin
            .on_request(&start, &ctx)
            .await?
            .ok_or("Missing sign-in response")?;
        let mut statuses = vec![response.status];
        let mut location = None;
        if mode == "form_post" {
            let body: Value = serde_json::from_slice(&response.body.bytes()?)?;
            let url = url::Url::parse(text(&body, "/url")?)?;
            let state = url
                .query_pairs()
                .find(|(key, _)| key == "state")
                .ok_or("Missing state")?
                .1
                .into_owned();
            let cookie = response
                .headers
                .get_all("set-cookie")
                .map(|value| value.split(';').next().ok_or("Missing state cookie"))
                .collect::<Result<Vec<_>, _>>()?
                .join("; ");
            assert!(!cookie.is_empty());
            let user = serde_json::to_string(user_input)?;
            let mut callback = AuthRequest::new(HttpMethod::Post, "/callback/apple");
            callback.headers.insert("cookie".into(), cookie.clone());
            callback
                .headers
                .insert("origin".into(), "http://localhost:3000".into());
            callback.headers.insert(
                "content-type".into(),
                "application/x-www-form-urlencoded".into(),
            );
            callback.body = Some(
                url::form_urlencoded::Serializer::new(String::new())
                    .append_pair("state", &state)
                    .append_pair("code", "ordinary-code")
                    .append_pair("user", &user)
                    .finish()
                    .into_bytes(),
            );
            let route = <OAuthPlugin as AuthPlugin<BundledSchema>>::routes(&plugin)
                .into_iter()
                .find(|route| route.matches(callback.method(), callback.path()))
                .ok_or("Missing Apple callback route")?;
            // Direct plugin dispatch requires the HTTP parser's decoded form input.
            callback.parse_http_body(&route.allowed_media_types)?;
            let redirect = plugin
                .on_request(&callback, &ctx)
                .await?
                .ok_or("Missing POST callback response")?;
            statuses.push(redirect.status);
            assert!(
                server
                    .requests
                    .lock()
                    .map_err(|error| error.to_string())?
                    .is_empty()
            );
            let target = url::Url::parse(
                redirect
                    .headers
                    .get("Location")
                    .ok_or("Missing callback Location")?,
            )?;
            assert_eq!(target.path(), "/api/auth/callback/apple");
            let query: std::collections::HashMap<String, String> =
                target.query_pairs().into_owned().collect();
            assert_eq!(query.get("state"), Some(&state));
            assert_eq!(query.get("code").map(String::as_str), Some("ordinary-code"));
            assert_eq!(query.get("user"), Some(&user));
            let mut complete = AuthRequest::new(HttpMethod::Get, "/callback/apple");
            complete.headers.insert("cookie".into(), cookie);
            complete.query = Some(serde_json::to_value(query)?);
            let response = plugin
                .on_request(&complete, &ctx)
                .await?
                .ok_or("Missing GET callback response")?;
            statuses.push(response.status);
            location = response.headers.get("Location").cloned();
        }
        assert_eq!(
            json!(statuses),
            *sample.get("status").ok_or("Missing status")?
        );
        assert_eq!(
            json!(location),
            *sample.get("location").ok_or("Missing location")?
        );
        let user = ctx
            .database
            .get_user_by_email(text(sample, "/claims/email")?)
            .await?
            .ok_or("Missing Apple user")?;
        let user_id = user.id.display_string()?;
        let accounts = ctx.database.get_user_accounts(&user_id).await?;
        let sessions = ctx.database.get_user_sessions(&user_id).await?;
        let account = accounts.first().ok_or("Missing Apple account")?;
        let session = serde_json::to_value(sessions.first().ok_or("Missing Apple session")?)?;
        let user = serde_json::to_value(user)?;
        let result = json!({
            "name":user.get("name"), "email":user.get("email"), "emailVerified":user.get("emailVerified"), "image":user.get("image"),
            "provider":account.provider_id.as_str(), "subject":account.account_id.as_str(), "accountCount":accounts.len(),
            "sessionCount":sessions.len(), "sessionMatchesUser":session.get("userId") == user.get("id"),
        });
        assert_eq!(result, *sample.get("result").ok_or("Missing result")?);
        assert_eq!(
            json!(captured.try_iter().collect::<Vec<_>>()),
            *sample.get("mapperInputs").ok_or("Missing mapper inputs")?
        );
        assert_eq!(called.try_iter().collect::<Vec<_>>(), ["map"]);
        let requests: Vec<_> = server
            .requests
            .lock()
            .map_err(|error| error.to_string())?
            .iter()
            .map(|path| match path.as_str() {
                "/jwks" => Ok("/auth/keys"),
                "/token" => Ok("/auth/token"),
                _ => Err(format!("Unexpected Apple test request: {path}")),
            })
            .collect::<Result<Vec<_>, _>>()?;
        assert_eq!(
            json!(requests),
            *sample.get("requests").ok_or("Missing requests")?
        );
    }
    Ok(())
}