aurabase 0.1.1

Official Rust SDK for Aurabase: high-performance open-source Backend-as-a-Service (BaaS)
Documentation
//! F3 — `sign_in_with_oauth` doit rendre le MÊME verdict qu'en JavaScript.
//!
//! Ce SDK recopiait `query_params` verbatim à la suite de `redirect_to` et `scopes`, dans un
//! `Vec` de paires. Deux défauts, tous deux silencieux :
//!
//!   1. un `redirect_to` passé par `query_params` apparaissait DEUX FOIS dans la query — et
//!      `aura-auth` refuse explicitement une clé répétée, donc 400 en pleine navigation ;
//!   2. `project_id`, `state`, `client_id`… partaient au serveur, qui les refuse de la même
//!      façon — l'utilisateur atterrissait sur du JSON au milieu d'une redirection.
//!
//! L'autorité du contrat est le BACKEND : l'endpoint de démarrage est public et appelable
//! sans SDK, une allowlist purement cliente ne protégerait rien. Ce fichier vérifie que la
//! copie Rust en est le miroir EXACT, en lisant la source du service.

use aurabase::create_client;
use aurabase::services::auth::{OAUTH_FORWARDABLE_QUERY_PARAMS, OAUTH_SDK_OWNED_QUERY_PARAMS};
use aurabase::types::OAuthSignInOptions;
use std::collections::HashMap;

const BASE: &str = "https://mon-projet.aurabase.cloud";

fn options(query_params: &[(&str, &str)]) -> OAuthSignInOptions {
    OAuthSignInOptions {
        redirect_to: None,
        scopes: None,
        query_params: Some(
            query_params
                .iter()
                .map(|(k, v)| (k.to_string(), v.to_string()))
                .collect::<HashMap<_, _>>(),
        ),
    }
}

#[allow(clippy::result_large_err)]
fn url(query_params: &[(&str, &str)]) -> Result<String, aurabase::error::AuraError> {
    create_client(BASE, "aura_anon_test", None)
        .auth()
        .sign_in_with_oauth("google", Some(options(query_params)))
}

#[test]
fn transmet_les_parametres_dexperience_legitimes() {
    let u = url(&[
        ("prompt", "consent"),
        ("login_hint", "a@b.c"),
        ("access_type", "offline"),
        ("hd", "ex.com"),
    ])
    .expect("paramètres d'expérience légitimes");
    for attendu in [
        "prompt=consent",
        "login_hint=a%40b.c",
        "access_type=offline",
        "hd=ex.com",
    ] {
        assert!(u.contains(attendu), "{attendu} absent de : {u}");
    }
}

#[test]
fn refuse_un_parametre_du_protocole_sans_rien_ignorer() {
    for cle in ["state", "client_id", "response_type", "scope", "nonce"] {
        let err = url(&[(cle, "x")])
            .expect_err("un paramètre du protocole est calculé par le serveur, jamais reçu");
        assert_eq!(err.code, "invalid_oauth_query_param", "{cle}");
        assert!(err.message.contains(cle), "{cle} : {}", err.message);
    }
}

#[test]
fn refuse_une_cle_qui_ecraserait_un_parametre_du_sdk() {
    // `redirect_to` est le cas qui comptait : il DÉDOUBLAIT la destination de connexion.
    for cle in OAUTH_SDK_OWNED_QUERY_PARAMS {
        let err = url(&[(cle, "https://attaquant.test/vol")])
            .expect_err("un paramètre construit par le SDK ne peut pas venir de l'appelant");
        assert_eq!(err.code, "invalid_oauth_query_param", "{cle}");
        assert!(err.message.contains(cle), "{cle} : {}", err.message);
    }
}

#[test]
fn refuse_une_cle_inconnue_en_nommant_lensemble_autorise() {
    for cle in ["utm_source", "Prompt", "State"] {
        let err = url(&[(cle, "x")]).expect_err("clé inconnue");
        assert!(err.message.contains(cle), "{cle} : {}", err.message);
        assert!(
            err.message.contains("prompt"),
            "le refus doit nommer l'ensemble autorisé : {}",
            err.message
        );
    }
}

#[test]
fn le_redirect_to_de_lappelant_survit_a_un_query_params_legitime() {
    let u = create_client(BASE, "aura_anon_test", None)
        .auth()
        .sign_in_with_oauth(
            "google",
            Some(OAuthSignInOptions {
                redirect_to: Some("https://app.test/cb".into()),
                scopes: None,
                query_params: Some(HashMap::from([(
                    "prompt".to_string(),
                    "consent".to_string(),
                )])),
            }),
        )
        .expect("options légitimes");
    assert!(u.contains("redirect_to=https%3A%2F%2Fapp.test%2Fcb"), "{u}");
    assert_eq!(
        u.matches("redirect_to=").count(),
        1,
        "une clé répétée est refusée en 400 par aura-auth : {u}"
    );
}

#[test]
fn lordre_des_parametres_est_stable() {
    // Une `HashMap` n'a pas d'ordre : sans tri, l'URL changeait d'une exécution à l'autre.
    let cas = &[("prompt", "consent"), ("login_hint", "a@b.c"), ("hd", "x")];
    let premier = url(cas).expect("légitime");
    for _ in 0..20 {
        assert_eq!(url(cas).expect("légitime"), premier);
    }
}

#[test]
fn le_chemin_ne_porte_ni_project_id_ni_parametre_de_projet() {
    let u = create_client(BASE, "aura_anon_test", None)
        .auth()
        .sign_in_with_oauth("google", None)
        .expect("aucun query_params");
    assert!(
        u.starts_with("https://mon-projet.aurabase.cloud/v1/auth/oauth/google/start"),
        "{u}"
    );
    assert!(!u.contains("project_id"), "{u}");
}

/// Anti-divergence : la source de vérité est l'allowlist Rust du service, pas cette copie.
#[test]
fn est_le_miroir_exact_de_lallowlist_du_backend() {
    // `cargo test` s'exécute depuis la racine du crate (`aurabase-rs/`).
    let chemin = std::path::Path::new("../services/aura-auth/src/handlers/oauth.rs").to_path_buf();
    let Ok(texte) = std::fs::read_to_string(&chemin) else {
        // Crate consommé hors du dépôt : rien à croiser.
        return;
    };
    let debut = texte
        .find("const OAUTH_FORWARDABLE_PARAMS")
        .expect("allowlist backend introuvable dans oauth.rs");
    // Sauter la DÉCLARATION DE TYPE `: [&str; 10]` — son crochet précède le littéral, et le
    // prendre pour le tableau rendait ce test vert sur une liste vide (mesuré : « allowlist
    // backend vide : [&str; 10] »). On part donc du `= [` qui ouvre la valeur.
    let bloc = &texte[debut..];
    let ouverture = bloc.find("= [").expect("littéral du tableau introuvable") + 2;
    let bloc = &bloc[ouverture..];
    let bloc = &bloc[..bloc.find(']').expect("fermeture du tableau") + 1];

    let mut backend: Vec<String> = bloc
        .split('"')
        .skip(1)
        .step_by(2)
        .map(str::to_string)
        .collect();
    backend.sort();
    assert!(!backend.is_empty(), "allowlist backend vide : {bloc}");

    let mut sdk: Vec<String> = OAUTH_FORWARDABLE_QUERY_PARAMS
        .iter()
        .map(|s| s.to_string())
        .collect();
    sdk.sort();
    assert_eq!(sdk, backend);
}