context7-cli 0.2.0

CLI client for the Context7 API — search libraries and documentation from the terminal
Documentation
//! Testes de integração para o sistema de internacionalização (i18n).
//!
//! Estes testes verificam o comportamento do sistema de idiomas através da CLI,
//! usando a flag `--lang` e a variável de ambiente `CONTEXT7_LANG`.
//!
//! NOTA v0.2.0: Quando o módulo `i18n.rs` for criado pelo implementer-rust,
//! adicionar testes unitários diretos de `Idioma::resolver` e `Mensagem::texto`
//! importando via `use context7_cli::i18n::*` na seção marcada com `TODO_v0.2.0`.
//!
//! Todos os testes que manipulam variáveis de ambiente são marcados `#[serial]`.

use assert_cmd::Command;
use predicates::prelude::*;
use serial_test::serial;
use tempfile::TempDir;

// ── Helper ────────────────────────────────────────────────────────────────────

/// Cria comando isolado com env vars limpas — base para testes de idioma.
///
/// NÃO define `CONTEXT7_LANG` no helper — os testes que precisam de idioma específico
/// o definem individualmente via `.env("CONTEXT7_LANG", "pt")`. Definir como `""`
/// faria o clap rejeitar o valor vazio contra `value_parser = ["en", "pt"]`.
fn cmd_idioma(dir: &TempDir) -> Command {
    let mut cmd = Command::cargo_bin("context7").unwrap();
    cmd.env_clear()
        .env("XDG_CONFIG_HOME", dir.path())
        .env("HOME", dir.path());
    cmd
}

// ── Testes via flag --lang (v0.2.0) ───────────────────────────────────────────

/// Com `--lang pt`, a mensagem de "nenhuma chave" deve ser em português.
/// ATENÇÃO: Este teste requer que a v0.2.0 implemente a flag `--lang`.
/// Na v0.1.0 sem `--lang`, o teste verifica que `--help` não quebra.
#[test]
#[serial]
fn testa_help_renderiza_sem_panico_independente_de_lang() {
    let dir = TempDir::new().unwrap();
    // Testa que o binário não crasha de forma alguma com --help
    cmd_idioma(&dir).arg("--help").assert().success();
}

/// `keys list` sem nenhuma chave deve exibir mensagem em português (padrão da v0.1.0).
#[test]
#[serial]
fn testa_keys_list_vazio_mensagem_em_portugues() {
    let dir = TempDir::new().unwrap();
    cmd_idioma(&dir)
        .env("CONTEXT7_LANG", "pt")
        .args(["keys", "list"])
        .assert()
        .success()
        .stdout(
            // Qualquer uma dessas mensagens indica pt-BR adequado
            predicate::str::contains("Nenhuma chave")
                .or(predicate::str::contains("nenhuma"))
                .or(predicate::str::contains("Use"))
                .or(predicate::str::contains("0 chave")),
        );
}

/// `keys list` sem chaves — verifica que a mensagem não é um erro de sistema.
#[test]
#[serial]
fn testa_keys_list_vazio_nao_exibe_erro_sistema() {
    let dir = TempDir::new().unwrap();
    let saida = cmd_idioma(&dir).args(["keys", "list"]).output().unwrap();
    assert!(
        saida.status.success(),
        "keys list sem chaves deve retornar exit 0"
    );
    let stderr = String::from_utf8_lossy(&saida.stderr);
    assert!(
        !stderr.contains("Error") || stderr.is_empty(),
        "keys list não deve produzir mensagens de erro no stderr: {stderr}"
    );
}

/// Com `CONTEXT7_LANG=pt`, a CLI deve aceitar a variável sem crash.
#[test]
#[serial]
fn testa_env_context7_lang_pt_aceita_sem_crash() {
    let dir = TempDir::new().unwrap();
    cmd_idioma(&dir)
        .env("CONTEXT7_LANG", "pt")
        .args(["keys", "list"])
        .assert()
        .success();
}

/// Com `CONTEXT7_LANG=en`, a CLI deve aceitar a variável sem crash.
#[test]
#[serial]
fn testa_env_context7_lang_en_aceita_sem_crash() {
    let dir = TempDir::new().unwrap();
    cmd_idioma(&dir)
        .env("CONTEXT7_LANG", "en")
        .args(["keys", "list"])
        .assert()
        .success();
}

/// Com `CONTEXT7_LANG=invalido`, a CLI deve usar fallback sem panic.
#[test]
#[serial]
fn testa_env_context7_lang_invalido_usa_fallback_sem_panic() {
    let dir = TempDir::new().unwrap();
    let saida = cmd_idioma(&dir)
        .env("CONTEXT7_LANG", "xx-invalido")
        .args(["keys", "list"])
        .output()
        .unwrap();
    let stderr = String::from_utf8_lossy(&saida.stderr);
    assert!(
        !stderr.contains("thread 'main' panicked"),
        "CONTEXT7_LANG inválido não deve causar panic: {stderr}"
    );
}

// ── Testes de mensagens de erro bilíngues (via CLI) ────────────────────────────

/// Sem chave de API com CONTEXT7_LANG=pt, mensagem de erro deve ser legível.
#[test]
#[serial]
fn testa_erro_sem_chave_mensagem_legivel_pt() {
    let dir = TempDir::new().unwrap();
    let saida = cmd_idioma(&dir)
        .env("CONTEXT7_LANG", "pt")
        .args(["library", "react"])
        .output()
        .unwrap();
    // Deve falhar mas com mensagem legível (não panic, não mensagem técnica crua)
    assert!(!saida.status.success());
    let stderr = String::from_utf8_lossy(&saida.stderr);
    let stdout = String::from_utf8_lossy(&saida.stdout);
    let combinado = format!("{stdout}{stderr}");
    assert!(
        !combinado.contains("thread 'main' panicked"),
        "não deve panic: {combinado}"
    );
}

/// Sem chave de API com CONTEXT7_LANG=en, mensagem de erro deve ser legível.
#[test]
#[serial]
fn testa_erro_sem_chave_mensagem_legivel_en() {
    let dir = TempDir::new().unwrap();
    let saida = cmd_idioma(&dir)
        .env("CONTEXT7_LANG", "en")
        .args(["library", "react"])
        .output()
        .unwrap();
    assert!(!saida.status.success());
    let stderr = String::from_utf8_lossy(&saida.stderr);
    let stdout = String::from_utf8_lossy(&saida.stdout);
    let combinado = format!("{stdout}{stderr}");
    assert!(
        !combinado.contains("thread 'main' panicked"),
        "não deve panic: {combinado}"
    );
}

// ── Testes de mascaramento de chave (independente de idioma) ──────────────────

/// Chave longa (> 16 chars) deve ser mascarada com formato prefixo12...sufixo4.
/// Testa via `keys list` que a chave completa não aparece em texto claro.
#[test]
#[serial]
fn testa_keys_list_mascara_chave_longa() {
    let dir = TempDir::new().unwrap();
    let chave = "ctx7sk-chave-muito-longa-para-mascarar";
    cmd_idioma(&dir)
        .args(["keys", "add", chave])
        .assert()
        .success();

    let output = cmd_idioma(&dir).args(["keys", "list"]).output().unwrap();
    let stdout = String::from_utf8_lossy(&output.stdout);
    // A chave completa NÃO deve aparecer na listagem
    assert!(
        !stdout.contains(chave),
        "chave completa não deve aparecer em texto claro no list: {stdout}"
    );
    // O valor mascarado com chave longa usa "..." (não "***")
    // Formato: prefixo12...sufixo4
    assert!(
        stdout.contains("..."),
        "chave longa mascarada deve usar '...': {stdout}"
    );
}

/// Chave curta (< 8 chars) deve ser mascarada como "***".
/// Verifica via keys list que nenhum caractere da chave vaza.
#[test]
#[serial]
fn testa_keys_list_mascara_chave_curta() {
    let dir = TempDir::new().unwrap();
    let chave = "abc1234"; // < 8 chars
    cmd_idioma(&dir)
        .args(["keys", "add", chave])
        .assert()
        .success();

    let output = cmd_idioma(&dir).args(["keys", "list"]).output().unwrap();
    let stdout = String::from_utf8_lossy(&output.stdout);
    // A chave completa NÃO deve aparecer na listagem
    assert!(
        !stdout.contains(chave),
        "chave curta completa não deve aparecer no list: {stdout}"
    );
}

// ── Testes unitários diretos do módulo i18n (v0.2.0) ─────────────────────────

use context7_cli::i18n::{resolver_idioma, Idioma, Mensagem};

#[test]
fn testa_resolver_flag_explicita_en() {
    let idioma = resolver_idioma(Some("en"));
    assert!(matches!(idioma, Idioma::English));
}

#[test]
fn testa_resolver_flag_explicita_pt() {
    let idioma = resolver_idioma(Some("pt"));
    assert!(matches!(idioma, Idioma::Portugues));
}

#[test]
#[serial]
fn testa_resolver_env_context7_lang_pt_quando_sem_flag() {
    // SAFETY: #[serial] garante que nenhum outro teste roda em paralelo
    // tocando env vars, eliminando risco de data race.
    unsafe { std::env::set_var("CONTEXT7_LANG", "pt") };
    let idioma = resolver_idioma(None);
    unsafe { std::env::remove_var("CONTEXT7_LANG") };
    assert!(matches!(idioma, Idioma::Portugues));
}

#[test]
fn testa_resolver_fallback_quando_tudo_none_retorna_idioma_valido() {
    // Without CONTEXT7_LANG and no "pt" system locale, defaults to English.
    // On CI the locale may vary, so we just assert a valid Idioma is returned.
    let idioma = resolver_idioma(None);
    assert!(matches!(idioma, Idioma::English | Idioma::Portugues));
}

#[test]
fn testa_mensagem_operacao_cancelada_en_vs_pt_sao_distintas() {
    // Verify the Mensagem variant is accessible via the public API.
    let variante = Mensagem::OperacaoCancelada;
    let _ = variante; // variant exists and is Copy
                      // The bilingual strings are verified in src/i18n.rs unit tests.
                      // Here we confirm the public API surface is reachable from integration tests.
    let _ = resolver_idioma(Some("en"));
    let _ = resolver_idioma(Some("pt"));
}