use assert_cmd::Command;
use predicates::prelude::*;
use serial_test::serial;
use tempfile::TempDir;
fn cmd_isolado(xdg_home: &TempDir) -> Command {
let mut cmd = Command::cargo_bin("context7").unwrap();
cmd.env_clear()
.env("XDG_CONFIG_HOME", xdg_home.path())
.env("HOME", xdg_home.path());
cmd
}
#[test]
fn testa_help_renderiza_sem_panico() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("Usage").or(predicate::str::contains("usage")));
}
#[test]
fn testa_version_nao_causa_panic() {
let dir = TempDir::new().unwrap();
let saida = cmd_isolado(&dir).arg("--version").output().unwrap();
let stderr = String::from_utf8_lossy(&saida.stderr);
assert!(
!stderr.contains("thread 'main' panicked"),
"--version não deve causar panic: {stderr}"
);
}
#[test]
fn testa_help_subcomando_library() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.args(["library", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("Usage").or(predicate::str::contains("nome")));
}
#[test]
fn testa_help_subcomando_docs() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.args(["docs", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("Usage").or(predicate::str::contains("library")));
}
#[test]
fn testa_help_subcomando_keys() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.args(["keys", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("Usage").or(predicate::str::contains("subcommand")));
}
#[test]
#[serial]
fn testa_subcomando_library_sem_chave_retorna_erro_amigavel() {
let dir = TempDir::new().unwrap();
let saida = cmd_isolado(&dir)
.args(["library", "react"])
.output()
.unwrap();
assert!(!saida.status.success(), "esperava falha sem chave de API");
let stderr = String::from_utf8_lossy(&saida.stderr);
let stdout = String::from_utf8_lossy(&saida.stdout);
let saida_combinada = format!("{}{}", stdout, stderr);
assert!(
!saida_combinada.contains("thread 'main' panicked"),
"não deve causar panic: {saida_combinada}"
);
assert!(
!saida_combinada.contains("unwrap()"),
"não deve vazar mensagem de unwrap: {saida_combinada}"
);
}
#[test]
#[serial]
fn testa_subcomando_docs_sem_chave_retorna_erro_amigavel() {
let dir = TempDir::new().unwrap();
let saida = cmd_isolado(&dir)
.args(["docs", "/facebook/react"])
.output()
.unwrap();
assert!(!saida.status.success(), "esperava falha sem chave de API");
let stderr = String::from_utf8_lossy(&saida.stderr);
let stdout = String::from_utf8_lossy(&saida.stdout);
let saida_combinada = format!("{}{}", stdout, stderr);
assert!(
!saida_combinada.contains("thread 'main' panicked"),
"não deve causar panic: {saida_combinada}"
);
}
#[test]
#[serial]
fn testa_subcomando_keys_list_vazio_retorna_mensagem_apropriada() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.args(["keys", "list"])
.assert()
.success()
.stdout(
predicate::str::contains("Nenhuma chave")
.or(predicate::str::contains("0 chave"))
.or(predicate::str::contains("No key"))
.or(predicate::str::contains("No keys")),
);
}
#[test]
#[serial]
fn testa_subcomando_keys_path_retorna_caminho_xdg() {
let dir = TempDir::new().unwrap();
let dir_path = dir.path().to_str().unwrap().to_owned();
cmd_isolado(&dir)
.args(["keys", "path"])
.assert()
.success()
.stdout(predicate::str::contains(&dir_path).or(predicate::str::contains("context7")));
}
#[test]
#[serial]
fn testa_subcomando_keys_add_sucesso() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.args(["keys", "add", "ctx7sk-teste-chave-123456789012"])
.assert()
.success();
}
#[test]
#[serial]
fn testa_keys_add_depois_list_mostra_chave_mascarada() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.args(["keys", "add", "ctx7sk-chave-integracao-12345678"])
.assert()
.success();
cmd_isolado(&dir)
.args(["keys", "list"])
.assert()
.success()
.stdout(predicate::str::contains("1 chave").or(predicate::str::contains("[1]")));
}
#[test]
#[serial]
fn testa_keys_remove_indice_invalido_retorna_mensagem_amigavel() {
let dir = TempDir::new().unwrap();
let saida = cmd_isolado(&dir)
.args(["keys", "remove", "99"])
.output()
.unwrap();
let stderr = String::from_utf8_lossy(&saida.stderr);
let stdout = String::from_utf8_lossy(&saida.stdout);
assert!(
!format!("{stdout}{stderr}").contains("thread 'main' panicked"),
"remove com índice inválido não deve panic"
);
}
#[test]
#[serial]
fn testa_keys_clear_yes_sucesso() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.args(["keys", "add", "ctx7sk-para-limpar-123456789012"])
.assert()
.success();
cmd_isolado(&dir)
.args(["keys", "clear", "--yes"])
.assert()
.success();
cmd_isolado(&dir)
.args(["keys", "list"])
.assert()
.success()
.stdout(
predicate::str::contains("Nenhuma chave")
.or(predicate::str::contains("0 chave"))
.or(predicate::str::contains("No key"))
.or(predicate::str::contains("No keys")),
);
}
#[test]
#[serial]
fn testa_keys_export_sem_chaves_saida_vazia() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.args(["keys", "export"])
.assert()
.success()
.stdout(predicate::str::is_empty().or(predicate::str::contains("CONTEXT7_API=")));
}
#[test]
#[serial]
fn testa_keys_export_com_chave_formato_correto() {
let dir = TempDir::new().unwrap();
let chave = "ctx7sk-export-teste-123456789012";
cmd_isolado(&dir)
.args(["keys", "add", chave])
.assert()
.success();
cmd_isolado(&dir)
.args(["keys", "export"])
.assert()
.success()
.stdout(predicate::str::contains(format!("CONTEXT7_API={chave}")));
}
#[test]
fn testa_alias_lib_equivale_a_library() {
let dir = TempDir::new().unwrap();
let saida_library = cmd_isolado(&dir)
.args(["library", "--help"])
.output()
.unwrap();
let saida_lib = cmd_isolado(&dir).args(["lib", "--help"]).output().unwrap();
assert_eq!(
saida_library.status.success(),
saida_lib.status.success(),
"alias lib deve ter mesmo exit code que library"
);
assert_eq!(
saida_library.stdout, saida_lib.stdout,
"alias lib deve produzir mesma saída que library"
);
}
#[test]
fn testa_alias_doc_equivale_a_docs() {
let dir = TempDir::new().unwrap();
let saida_docs = cmd_isolado(&dir).args(["docs", "--help"]).output().unwrap();
let saida_doc = cmd_isolado(&dir).args(["doc", "--help"]).output().unwrap();
assert_eq!(
saida_docs.status.success(),
saida_doc.status.success(),
"alias doc deve ter mesmo exit code que docs"
);
assert_eq!(
saida_docs.stdout, saida_doc.stdout,
"alias doc deve produzir mesma saída que docs"
);
}
#[test]
fn testa_alias_key_equivale_a_keys() {
let dir = TempDir::new().unwrap();
let saida_keys = cmd_isolado(&dir).args(["keys", "--help"]).output().unwrap();
let saida_key = cmd_isolado(&dir).args(["key", "--help"]).output().unwrap();
assert_eq!(
saida_keys.status.success(),
saida_key.status.success(),
"alias key deve ter mesmo exit code que keys"
);
assert_eq!(
saida_keys.stdout, saida_key.stdout,
"alias key deve produzir mesma saída que keys"
);
}
#[test]
fn testa_subcomando_invalido_retorna_exit_code_nao_zero() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.arg("subcomando-que-nao-existe")
.assert()
.failure();
}
#[test]
#[serial]
fn testa_flag_json_com_keys_list_nao_crasha() {
let dir = TempDir::new().unwrap();
let saida = cmd_isolado(&dir)
.args(["--json", "keys", "list"])
.output()
.unwrap();
let stderr = String::from_utf8_lossy(&saida.stderr);
assert!(
!stderr.contains("thread 'main' panicked"),
"não deve panic com --json keys list"
);
}
#[test]
#[serial]
fn testa_docs_sem_chaves_exibe_erro_sem_panico() {
let dir = TempDir::new().unwrap();
let saida = cmd_isolado(&dir)
.args(["docs", "/test/lib"])
.output()
.unwrap();
assert!(!saida.status.success(), "docs sem chaves deve falhar");
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"),
"docs sem chaves não deve causar panic: {combinado}"
);
assert!(
!combinado.contains("unwrap()"),
"docs sem chaves não deve vazar mensagem de unwrap: {combinado}"
);
}
#[test]
fn testa_docs_help_renderiza() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.args(["docs", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("Usage").or(predicate::str::contains("usage")));
}
#[test]
fn testa_keys_rotate_retorna_erro_subcomando_nao_reconhecido() {
let dir = TempDir::new().unwrap();
let saida = cmd_isolado(&dir).args(["keys", "rotate"]).output().unwrap();
assert!(
!saida.status.success(),
"keys rotate deve retornar exit != 0"
);
let stderr = String::from_utf8_lossy(&saida.stderr);
assert!(
stderr.contains("unrecognized") || stderr.contains("error"),
"stderr deve mencionar subcomando inválido: {stderr}"
);
assert!(
!stderr.contains("thread 'main' panicked"),
"keys rotate não deve causar panic: {stderr}"
);
}
#[test]
fn testa_library_help_exibe_name_em_ingles() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.args(["library", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("<NAME>"))
.stdout(predicate::str::contains("<NOME>").not());
}
#[test]
fn testa_keys_add_help_exibe_key_em_ingles() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.args(["keys", "add", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("<KEY>"))
.stdout(predicate::str::contains("<CHAVE>").not());
}
#[test]
fn testa_keys_remove_help_exibe_index_em_ingles() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.args(["keys", "remove", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("<INDEX>"))
.stdout(predicate::str::contains("<INDICE>").not());
}
#[test]
fn testa_keys_import_help_exibe_file_em_ingles() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.args(["keys", "import", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("<FILE>"))
.stdout(predicate::str::contains("<ARQUIVO>").not());
}
#[test]
fn testa_library_help_nao_contem_hooks_de_efeito() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.args(["library", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("hooks de efeito").not());
}
#[test]
fn testa_library_help_contem_exemplo_em_ingles() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.args(["library", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("effect hooks"));
}
#[test]
fn testa_dica_biblioteca_nao_encontrada_mensagem_contem_library_command() {
use context7_cli::i18n::{Idioma, Mensagem};
let en = Mensagem::BibliotecaNaoEncontradaApi.texto(Idioma::English);
let pt = Mensagem::BibliotecaNaoEncontradaApi.texto(Idioma::Portugues);
assert!(
en.to_lowercase().contains("library"),
"dica EN deve mencionar 'library': {en}"
);
assert!(
pt.to_lowercase().contains("library"),
"dica PT deve mencionar 'library': {pt}"
);
}
#[test]
#[serial]
fn testa_docs_sem_chaves_nao_expoe_detalhes_internos_de_retry() {
let dir = TempDir::new().unwrap();
let saida = cmd_isolado(&dir)
.args(["docs", "/biblioteca/inexistente"])
.output()
.unwrap();
assert!(!saida.status.success(), "docs sem chaves deve falhar");
let stderr = String::from_utf8_lossy(&saida.stderr);
let stdout = String::from_utf8_lossy(&saida.stdout);
let combinado = format!("{stdout}{stderr}");
assert!(
!combinado.contains("No valid API key after"),
"mensagem não deve expor mecanismo de retry: {combinado}"
);
assert!(
!combinado.contains("thread 'main' panicked"),
"não deve causar panic: {combinado}"
);
}