context7-cli 0.2.2

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"));
}

// ── Testes das novas variantes v0.2.1 ────────────────────────────────────────

use context7_cli::i18n::t;

/// `FalhaBuscarDocumentacao` EN e PT são distintas e não-vazias.
///
/// Usa `en()` e `pt()` via funções de localização do módulo i18n para
/// comparar sem alterar o idioma global (OnceLock — imutável após set).
#[test]
fn testa_mensagem_falha_buscar_documentacao_acessivel_e_nao_vazia() {
    // A API pública expõe t() que usa idioma_atual().
    // Verificamos que a variante existe e retorna string não-vazia no idioma padrão.
    let texto = t(Mensagem::FalhaBuscarDocumentacao);
    assert!(
        !texto.is_empty(),
        "FalhaBuscarDocumentacao deve ser não-vazia"
    );
    // Verifica que a string é semanticamente relacionada a "documentação" ou "fetch"
    let texto_lower = texto.to_lowercase();
    assert!(
        texto_lower.contains("doc")
            || texto_lower.contains("fetch")
            || texto_lower.contains("falha")
            || texto_lower.contains("failed"),
        "FalhaBuscarDocumentacao deve mencionar doc/fetch/falha/failed: {texto}"
    );
}

/// `FalhaBuscarBiblioteca` está acessível e retorna string não-vazia.
#[test]
fn testa_mensagem_falha_buscar_biblioteca_acessivel_e_nao_vazia() {
    let texto = t(Mensagem::FalhaBuscarBiblioteca);
    assert!(
        !texto.is_empty(),
        "FalhaBuscarBiblioteca deve ser não-vazia"
    );
}

/// `FalhaCriarClienteHttp` está acessível e retorna string não-vazia.
#[test]
fn testa_mensagem_falha_criar_cliente_http_acessivel_e_nao_vazia() {
    let texto = t(Mensagem::FalhaCriarClienteHttp);
    assert!(
        !texto.is_empty(),
        "FalhaCriarClienteHttp deve ser não-vazia"
    );
}

/// `FalhaSerializarJson` está acessível e retorna string não-vazia.
#[test]
fn testa_mensagem_falha_serializar_json_acessivel_e_nao_vazia() {
    let texto = t(Mensagem::FalhaSerializarJson);
    assert!(!texto.is_empty(), "FalhaSerializarJson deve ser não-vazia");
}

/// `SemDocumentacaoDisponivel` está acessível e retorna string não-vazia.
#[test]
fn testa_mensagem_sem_documentacao_disponivel_acessivel_e_nao_vazia() {
    let texto = t(Mensagem::SemDocumentacaoDisponivel);
    assert!(
        !texto.is_empty(),
        "SemDocumentacaoDisponivel deve ser não-vazia"
    );
}

// ── Testes das novas variantes v0.2.2 ────────────────────────────────────────

/// Nova variante v0.2.2: `BibliotecaNaoEncontradaApi` deve ter traduções EN e PT
/// distintas, não-vazias e semanticamente coerentes.
///
/// Usa `Mensagem::texto(idioma)` diretamente para testar ambos os idiomas sem
/// depender do OnceLock global (determinístico, sem efeito colateral de estado).
#[test]
fn testa_mensagem_biblioteca_nao_encontrada_api_en_pt() {
    let en = Mensagem::BibliotecaNaoEncontradaApi.texto(Idioma::English);
    let pt = Mensagem::BibliotecaNaoEncontradaApi.texto(Idioma::Portugues);

    assert!(
        !en.is_empty(),
        "EN BibliotecaNaoEncontradaApi não deve ser vazia"
    );
    assert!(
        !pt.is_empty(),
        "PT BibliotecaNaoEncontradaApi não deve ser vazia"
    );
    assert_ne!(en, pt, "EN e PT devem ser strings diferentes (bilíngue)");
    assert!(
        en.to_lowercase().contains("library") || en.to_lowercase().contains("not found"),
        "EN deve mencionar 'library' ou 'not found', obteve: {en}"
    );
    assert!(
        pt.to_lowercase().contains("biblioteca") || pt.to_lowercase().contains("encontrada"),
        "PT deve mencionar 'biblioteca' ou 'encontrada', obteve: {pt}"
    );
}