aurabase 0.1.1

Official Rust SDK for Aurabase: high-performance open-source Backend-as-a-Service (BaaS)
Documentation
//! Régression prouvée en conditions réelles (stack locale, contrat backend
//! `services/aura-storage/src/handlers/manage.rs`) : `copy()`/`move_object()`
//! préfixaient les clés du `bucket_id` alors que le backend les préfixe DÉJÀ,
//! avec le bucket issu du path :
//!   copy_file : `let src_minio_key = format!("{}/{}", bucket_name, body.src_key);`
//!   move_file : idem.
//! D'où une clé physique `avatars/avatars/<uuid>.txt`, qui n'existe pas — échec
//! systématique « Copie S3: service error ». Le contrat backend attend une clé
//! RELATIVE au bucket, comme pour upload/download/remove/get_metadata — et
//! comme la clé que `upload()` renvoie (upload.rs strip déjà `{bucket_name}/`).
//!
//! `list()` a le même défaut de classe : le backend ne retire que le préfixe
//! `{project_id}/` (s3.rs `strip_project_prefix`), donc `FileEntry.name` reste
//! préfixé du bucket logique — incohérent avec la clé relative renvoyée par
//! `upload()`. Ce module vérifie que le SDK Rust la rend relative, comme le
//! SDK JS (`StorageService.ts` `list()`).

use aurabase::create_client;
use wiremock::matchers::{body_json, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};

fn test_client(base_url: &str) -> aurabase::AuraClient {
    create_client(base_url, "aura_anon_test", None)
}

#[tokio::test]
async fn copy_sends_keys_relative_to_bucket_not_prefixed() {
    let server = MockServer::start().await;
    Mock::given(method("POST"))
        .and(path("/v1/storage/avatars/copy"))
        .and(body_json(serde_json::json!({
            "src_key": "3f2a.png",
            "dst_key": "copie.png",
        })))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "data": { "key": "copie.png", "bucket": "avatars" }
        })))
        .mount(&server)
        .await;

    let client = test_client(&server.uri());
    let res = client
        .storage()
        .copy("avatars", "3f2a.png", "copie.png")
        .await
        .expect("copy() a échoué");

    assert!(
        res.error.is_none(),
        "copy() a renvoyé une erreur — la clé envoyée ne correspond pas à \
         celle attendue par le mock (donc probablement encore préfixée) : {:?}",
        res.error
    );
}

#[tokio::test]
async fn move_object_sends_keys_relative_to_bucket_not_prefixed() {
    let server = MockServer::start().await;
    Mock::given(method("POST"))
        .and(path("/v1/storage/avatars/move"))
        .and(body_json(serde_json::json!({
            "src_key": "3f2a.png",
            "dst_key": "deplace.png",
        })))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "data": { "key": "deplace.png", "bucket": "avatars" }
        })))
        .mount(&server)
        .await;

    let client = test_client(&server.uri());
    let res = client
        .storage()
        .move_object("avatars", "3f2a.png", "deplace.png")
        .await
        .expect("move_object() a échoué");

    assert!(
        res.error.is_none(),
        "move_object() a renvoyé une erreur — la clé envoyée ne correspond \
         pas à celle attendue par le mock (donc probablement encore \
         préfixée) : {:?}",
        res.error
    );
}

#[tokio::test]
async fn copy_accepts_key_roundtripped_from_upload() {
    // Aller-retour réaliste : upload() renvoie une clé RELATIVE ; copy() doit
    // l'accepter telle quelle sans la re-préfixer.
    let server = MockServer::start().await;
    let uploaded_key = "129cf0e4-a03d-4d98-bffb-6e59d55152ad.txt";
    Mock::given(method("POST"))
        .and(path("/v1/storage/docs/copy"))
        .and(body_json(serde_json::json!({
            "src_key": uploaded_key,
            "dst_key": "archive.txt",
        })))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "data": { "key": "archive.txt", "bucket": "docs" }
        })))
        .mount(&server)
        .await;

    let client = test_client(&server.uri());
    let res = client
        .storage()
        .copy("docs", uploaded_key, "archive.txt")
        .await
        .expect("copy() a échoué");

    assert!(res.error.is_none(), "{:?}", res.error);
}

#[tokio::test]
async fn list_strips_logical_bucket_prefix_from_key() {
    // Le backend (s3.rs strip_project_prefix) ne retire que le préfixe
    // `{project_id}/` : la clé renvoyée par list_objects reste préfixée du
    // bucket logique. Le SDK doit la rendre relative, comme la clé
    // qu'upload() renvoie, pour permettre le passage direct list() -> copy()
    // /download()/remove().
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/v1/storage/avatars"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "data": [
                {
                    "key": "avatars/1.png",
                    "bucket": "avatars",
                    "content_type": "image/png",
                    "size": 3
                }
            ]
        })))
        .mount(&server)
        .await;

    let client = test_client(&server.uri());
    let res = client
        .storage()
        .list("avatars", None, None, None)
        .await
        .expect("list() a échoué");

    let files = res.data.expect("aucune donnée renvoyée par list()");
    assert_eq!(
        files[0].name, "1.png",
        "la clé renvoyée par list() doit être relative au bucket, comme \
         celle renvoyée par upload() — reçu : {:?}",
        files[0].name
    );
}

#[tokio::test]
async fn list_leaves_key_without_bucket_prefix_untouched() {
    // Un enregistrement dont la clé physique ne porte, par accident ou par
    // construction future, aucun préfixe `{bucket}/` ne doit pas être tronqué
    // à tort (garde-fou : strip_prefix seulement, jamais un split naïf).
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/v1/storage/avatars"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "data": [
                {
                    "key": "1.png",
                    "bucket": "avatars",
                    "content_type": "image/png",
                    "size": 3
                }
            ]
        })))
        .mount(&server)
        .await;

    let client = test_client(&server.uri());
    let res = client
        .storage()
        .list("avatars", None, None, None)
        .await
        .expect("list() a échoué");

    let files = res.data.expect("aucune donnée renvoyée par list()");
    assert_eq!(files[0].name, "1.png");
}

// ─── N7 — `get_public_url` doit AVERTIR ──────────────────────────────────────
//
// Le README du SDK JS a été corrigé après une affirmation fausse « dans le sens dangereux »
// (« cette URL exige quand même une clé API ») : un bucket `public: true` n'est PAS protégé par
// la clé, le gateway laisse passer les `GET` d'objet dépourvus de clé. Ce SDK n'a jamais porté
// la fausse promesse, mais n'a jamais porté l'avertissement non plus — sur une méthode qui
// fabrique une URL réellement publique, c'est un silence dangereux, pas une neutralité.
//
// Les commentaires de documentation Rust ne sont pas lisibles à l'exécution : on assertionne
// donc sur la SOURCE, embarquée à la compilation.
mod n7_avertissement_url_publique {
    const SOURCE: &str = include_str!("../src/services/storage.rs");

    /// Extrait le bloc `///` qui précède immédiatement `pub fn get_public_url`.
    fn doc_de_get_public_url() -> String {
        let idx = SOURCE
            .find("pub fn get_public_url")
            .expect("get_public_url doit exister");
        // `SOURCE[..idx]` se termine AU MILIEU de la ligne de signature (juste avant
        // `pub fn`) : cette ligne partielle ne commence pas par `///` et arrêterait la
        // remontée immédiatement. On la retire avant de remonter les lignes de doc.
        let avant: Vec<&str> = SOURCE[..idx].lines().collect();
        avant
            .iter()
            .rev()
            .skip(1)
            .take_while(|l| l.trim_start().starts_with("///"))
            .copied()
            .collect::<Vec<_>>()
            .join("\n")
            .to_lowercase()
    }

    #[test]
    fn get_public_url_est_documentee() {
        assert!(
            !doc_de_get_public_url().is_empty(),
            "une méthode qui fabrique une URL publique ne peut pas rester sans documentation"
        );
    }

    #[test]
    fn la_doc_avertit_que_lurl_est_reellement_publique() {
        let doc = doc_de_get_public_url();
        assert!(doc.contains("public"), "{doc}");
        // Exiger la formulation VRAIE, pas la simple présence de « sans » et « clé api » :
        // ces deux mots figurent aussi dans la phrase sur l'URL signée, si bien qu'un
        // garde-fou basé sur eux restait vert même en réintroduisant la fausse promesse.
        assert!(
            doc.contains("sans aucune clé api"),
            "l'avertissement doit dire que l'URL répond SANS AUCUNE clé API : {doc}"
        );
        assert!(
            !doc.contains("exige quand même une clé api"),
            "ne jamais réaffirmer qu'une clé API est exigée : {doc}"
        );
    }

    #[test]
    fn la_doc_oriente_vers_lurl_signee_pour_un_bucket_prive() {
        let doc = doc_de_get_public_url();
        assert!(
            doc.contains("create_signed_url") || doc.contains("signée"),
            "l'alternative pour un partage temporaire doit être nommée : {doc}"
        );
    }
}