puerto 0.19.0

Puerto — Rust full-stack DDD framework CLI. Scaffolds Clean Architecture projects with auto-wired DI, SQLx support, and zero manual wiring. Formerly puerto-framework.
//! Yours: `puerto new --auth` writes this file once.

use poem_openapi::{OpenApi, payload::Json};
use puerto_web::auth::BearerAuth;

use crate::api::me::dto::MeDto;
use crate::api::tags::ApiTags;

/// `GET /me`: what the frontend asks after sign-in. No or a bad token is answered by `BearerAuth`
/// before the handler runs (401 `auth.unauthenticated`, 503 `auth.unavailable`).
pub struct MeApi;

#[OpenApi]
impl MeApi {
    /// The signed-in caller: their user id, profile and roles
    #[oai(path = "/me", method = "get", tag = "ApiTags::Me")]
    async fn me(&self, auth: BearerAuth) -> Json<MeDto> {
        Json(MeDto::from_principal(&auth.0))
    }
}

#[cfg(test)]
mod tests {
    use std::sync::Arc;

    use poem::{EndpointExt, Route, http::StatusCode, test::TestClient};
    use poem_openapi::OpenApiService;
    use puerto_core::auth::mocks::MockAuthenticator;
    use puerto_core::auth::{AuthError, Authenticator, Principal};
    use uuid::Uuid;

    use super::*;

    const USER_ID: Uuid = Uuid::from_u128(0x6b1f_0c2e_8d4a_4e51_9a3b_2f0d_7c6e_1a42);

    /// Accepts the token `ada`, rejects anything else.
    fn client() -> TestClient<impl poem::Endpoint> {
        let mut authenticator = MockAuthenticator::new();
        authenticator
            .expect_authenticate()
            .returning(|token| match token {
                "ada" => Ok(Principal::new(USER_ID, "ada-subject")
                    .with_email("ada@example.com")
                    .with_name("Ada Lovelace")
                    .with_roles(["admin"])),
                _ => Err(AuthError::Invalid),
            });
        let authenticator: Arc<dyn Authenticator> = Arc::new(authenticator);
        TestClient::new(
            Route::new()
                .nest("/", OpenApiService::new(MeApi, "test", "1.0"))
                .data(authenticator),
        )
    }

    #[tokio::test]
    async fn should_answer_the_caller_when_the_token_is_accepted() {
        // Act
        let response = client()
            .get("/me")
            .header("Authorization", "Bearer ada")
            .send()
            .await;

        // Assert
        response.assert_status_is_ok();
        let body = response.json().await;
        let me = body.value().object();
        me.get("id").assert_string(&USER_ID.to_string());
        me.get("email").assert_string("ada@example.com");
        me.get("name").assert_string("Ada Lovelace");
        me.get("roles").assert_string_array(&["admin"]);
    }

    #[tokio::test]
    async fn should_answer_401_when_there_is_no_token() {
        // Act
        let response = client().get("/me").send().await;

        // Assert
        response.assert_status(StatusCode::UNAUTHORIZED);
        response
            .json()
            .await
            .value()
            .object()
            .get("name")
            .assert_string("auth.unauthenticated");
    }

    #[tokio::test]
    async fn should_answer_401_when_the_token_is_rejected() {
        // Act
        let response = client()
            .get("/me")
            .header("Authorization", "Bearer forged")
            .send()
            .await;

        // Assert
        response.assert_status(StatusCode::UNAUTHORIZED);
    }
}