pg-api 0.3.13

A high-performance PostgreSQL REST API driver with rate limiting, connection pooling, and observability
//! Testes de integração para higiene das conexões do pool.
//!
//! Regressão do incidente de 21/09/2026: quando o cliente HTTP desconectava no
//! meio de uma query, a conexão voltava ao pool com a transação aberta/abortada
//! (`idle in transaction (aborted)`) e o próximo request que a pegava falhava no
//! `BEGIN` com `25P02 current transaction is aborted`.
//!
//! Como rodar:
//! ```bash
//! # servidor de teste (ver README, seção de desenvolvimento)
//! PG_API_TEST_URL=http://127.0.0.1:8581 \
//! PG_API_TEST_KEY=sk_test_hygiene \
//! PG_API_TEST_DB=pgapi_hygiene_test \
//! cargo test --test connection_hygiene
//! ```
//!
//! Sem `PG_API_TEST_URL` os testes usam `http://localhost:8580` e, se o servidor
//! não estiver no ar, são pulados (mesma convenção dos demais testes).

use std::time::Duration;

fn base_url() -> String {
    std::env::var("PG_API_TEST_URL").unwrap_or_else(|_| "http://localhost:8580".to_string())
}

fn api_key() -> String {
    std::env::var("PG_API_TEST_KEY").unwrap_or_else(|_| "sk_test_key".to_string())
}

fn database() -> String {
    std::env::var("PG_API_TEST_DB").unwrap_or_else(|_| "postgres".to_string())
}

fn client() -> reqwest::Client {
    reqwest::Client::builder()
        .timeout(Duration::from_secs(30))
        .build()
        .expect("cliente HTTP")
}

/// Executa uma query e devolve `(success, mensagem_de_erro)`.
async fn query(client: &reqwest::Client, sql: &str, timeout_ms: u64) -> Option<(bool, String)> {
    let response = client
        .post(format!("{}/v1/query", base_url()))
        .header("X-API-Key", api_key())
        .json(&serde_json::json!({
            "database": database(),
            "query": sql,
            "options": { "timeout_ms": timeout_ms }
        }))
        .send()
        .await
        .ok()?;
    let body: serde_json::Value = response.json().await.ok()?;
    let success = body
        .get("success")
        .and_then(|v| v.as_bool())
        .unwrap_or(false);
    let message = body
        .get("error")
        .and_then(|e| e.get("message"))
        .and_then(|m| m.as_str())
        .unwrap_or("")
        .to_string();
    Some((success, message))
}

/// Mensagens de erro do Postgres devem trazer o SQLSTATE, não um `"db error"` opaco.
#[tokio::test]
async fn erro_de_query_preserva_sqlstate() {
    let client = client();
    let Some((success, message)) = query(&client, "SELECT coluna_inexistente_hygiene", 5_000).await
    else {
        println!("⚠️  Servidor não está rodando, pulando teste");
        return;
    };
    assert!(!success, "query inválida deveria falhar");
    assert!(
        message.contains("SQLSTATE"),
        "mensagem sem SQLSTATE (mascarada como 'db error'?): {message}"
    );
}

/// Cancelamento do cliente não pode envenenar o pool.
///
/// 1. dispara uma query longa e aborta o request antes dela terminar;
/// 2. aguarda o `statement_timeout` do servidor;
/// 3. as queries seguintes precisam funcionar — antes do fix a primeira caía em
///    `25P02 current transaction is aborted`.
#[tokio::test]
async fn cancelamento_do_cliente_nao_envenena_o_pool() {
    let client = client();
    if query(&client, "SELECT 1", 5_000).await.is_none() {
        println!("⚠️  Servidor não está rodando, pulando teste");
        return;
    }

    // Query longa com timeout curto: o cliente desconecta em 300ms.
    let aborting = tokio::time::timeout(
        Duration::from_millis(300),
        client
            .post(format!("{}/v1/query", base_url()))
            .header("X-API-Key", api_key())
            .json(&serde_json::json!({
                "database": database(),
                "query": "SELECT pg_sleep(30)",
                "options": { "timeout_ms": 3000 }
            }))
            .send(),
    )
    .await;
    assert!(aborting.is_err(), "a query longa não deveria terminar");

    // Espera o statement_timeout (3s) encerrar a query abandonada no servidor.
    tokio::time::sleep(Duration::from_secs(5)).await;

    for attempt in 1..=5 {
        let Some((success, message)) = query(&client, "SELECT 1 AS ok", 10_000).await else {
            panic!("sem resposta do servidor na tentativa {attempt}");
        };
        assert!(
            success,
            "conexão envenenada devolvida ao pool na tentativa {attempt}: {message}"
        );
    }
}