use assert_cmd::Command;
use predicates::prelude::*;
use serial_test::serial;
use tempfile::TempDir;
#[allow(deprecated)] fn cmd_isolado(xdg_home: &TempDir) -> Command {
let mut cmd = Command::cargo_bin("context7").unwrap();
cmd.env_clear()
.env("CONTEXT7_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}"
);
}
#[test]
fn testa_docs_help_exibe_alias_q_e_long_query() {
let dir = TempDir::new().unwrap();
let saida = cmd_isolado(&dir).args(["docs", "--help"]).output().unwrap();
assert!(
saida.status.success(),
"docs --help deve terminar com sucesso"
);
let stdout = String::from_utf8_lossy(&saida.stdout);
assert!(
stdout.contains("-q") && stdout.contains("--query"),
"help de docs deve exibir '-q, --query': {stdout}"
);
}
#[test]
fn testa_docs_alias_q_e_aceito_como_argumento_valido() {
let dir = TempDir::new().unwrap();
let saida = cmd_isolado(&dir)
.env("CONTEXT7_API_KEYS", "ctx7sk-fake-key-12345678901")
.args(["docs", "/reactjs/react.dev", "-q", "hooks"])
.output()
.unwrap();
let stderr = String::from_utf8_lossy(&saida.stderr);
assert!(
!stderr.contains("unexpected argument"),
"'-q' não deve ser rejeitado como argumento desconhecido: {stderr}"
);
assert!(
!stderr.contains("thread 'main' panicked"),
"não deve causar panic: {stderr}"
);
}
#[test]
fn testa_keys_import_arquivo_inexistente_mensagem_util() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.args(["keys", "import", "/tmp/arquivo-que-nao-existe-v026.env"])
.assert()
.failure()
.stderr(
predicate::str::contains("arquivo")
.or(predicate::str::contains("file"))
.or(predicate::str::contains("No such")),
);
}
#[test]
fn testa_keys_import_arquivo_sem_chaves_context7_mensagem_util() {
let dir = TempDir::new().unwrap();
let arquivo_invalido = dir.path().join("invalido.env");
std::fs::write(
&arquivo_invalido,
"LIXO=nao_e_chave_context7\nOUTRA_VAR=valor\n",
)
.unwrap();
cmd_isolado(&dir)
.args(["keys", "import", arquivo_invalido.to_str().unwrap()])
.assert()
.failure()
.stderr(predicate::str::contains("CONTEXT7_API"));
}
#[test]
#[serial]
fn testa_keys_add_duplicata_exibe_aviso_de_chave_existente() {
let dir = TempDir::new().unwrap();
let chave = "ctx7sk-dedup-regression-v026-abc";
cmd_isolado(&dir)
.args(["keys", "add", chave])
.assert()
.success();
let saida = cmd_isolado(&dir)
.args(["keys", "add", chave])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&saida.stdout);
let stderr = String::from_utf8_lossy(&saida.stderr);
let combinado = format!("{stdout}{stderr}");
assert!(
combinado.to_lowercase().contains("existe")
|| combinado.to_lowercase().contains("exists")
|| combinado.to_lowercase().contains("already")
|| combinado.to_lowercase().contains("ignor")
|| combinado.to_lowercase().contains("skip"),
"segunda adição deve exibir aviso de duplicata. stdout='{stdout}' stderr='{stderr}'"
);
assert!(
!combinado.contains("thread 'main' panicked"),
"não deve causar panic: {combinado}"
);
}
#[test]
#[serial]
fn testa_regressao_qa028_fail1_import_sem_chaves_erro_contem_caminho() {
let dir = TempDir::new().unwrap();
let arquivo_sem_chave = dir.path().join("sem_contexto.env");
std::fs::write(
&arquivo_sem_chave,
"FOO=bar\nBAZ=qux\n# sem CONTEXT7_API=\n",
)
.unwrap();
let saida = cmd_isolado(&dir)
.args(["keys", "import", arquivo_sem_chave.to_str().unwrap()])
.output()
.unwrap();
assert!(
!saida.status.success(),
"keys import sem CONTEXT7_API= deve falhar com exit != 0"
);
let stderr = String::from_utf8_lossy(&saida.stderr);
assert!(
stderr.contains("sem_contexto.env"),
"stderr deve conter o nome do arquivo. stderr='{stderr}'"
);
assert!(
stderr.contains("CONTEXT7_API"),
"stderr deve mencionar CONTEXT7_API. stderr='{stderr}'"
);
assert!(
!stderr.contains("thread 'main' panicked"),
"não deve causar panic. stderr='{stderr}'"
);
}
#[test]
#[serial]
fn testa_regressao_qa028_fail1_import_apenas_comentarios_falha_com_exit1() {
let dir = TempDir::new().unwrap();
let arquivo_comentarios = dir.path().join("comentarios.env");
std::fs::write(
&arquivo_comentarios,
"# Este arquivo é só comentários\n# CONTEXT7_API=nao_conta\n",
)
.unwrap();
cmd_isolado(&dir)
.args(["keys", "import", arquivo_comentarios.to_str().unwrap()])
.assert()
.failure()
.stderr(predicate::str::contains("CONTEXT7_API"));
}
#[test]
#[serial]
fn testa_regressao_qa028_fail3_chave_curta_exibe_asteriscos() {
let dir = TempDir::new().unwrap();
let chave_curta = "ctx7sk-ab";
cmd_isolado(&dir)
.args(["keys", "add", chave_curta])
.assert()
.success();
let saida = cmd_isolado(&dir).args(["keys", "list"]).output().unwrap();
let stdout = String::from_utf8_lossy(&saida.stdout);
assert!(
!stdout.contains(chave_curta),
"chave curta não deve aparecer sem mascaramento. stdout='{stdout}'"
);
assert!(
stdout.contains("***"),
"chave curta deve ser exibida como '***'. stdout='{stdout}'"
);
}
#[test]
#[allow(deprecated)] #[serial]
fn testa_regressao_qa028_partial1_context7_home_path_traversal_usa_default() {
let dir = TempDir::new().unwrap();
let saida = Command::cargo_bin("context7")
.unwrap()
.env_clear()
.env("CONTEXT7_HOME", "../../../etc")
.env("HOME", dir.path())
.args(["keys", "path"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&saida.stdout);
let stderr = String::from_utf8_lossy(&saida.stderr);
assert!(
!stdout.contains("../../../etc"),
"path traversal não deve aparecer na saída. stdout='{stdout}'"
);
assert!(
!stdout.contains("/etc/config.toml"),
"traversal para /etc não deve funcionar. stdout='{stdout}'"
);
assert!(
!stderr.contains("thread 'main' panicked"),
"não deve causar panic. stderr='{stderr}'"
);
}
#[test]
fn testa_regressao_v040_version_string_correta() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.arg("--version")
.assert()
.success()
.stdout(predicate::str::contains(env!("CARGO_PKG_VERSION")));
}
#[test]
#[serial]
fn testa_regressao_v028_fail1_import_erro_nao_duplica_mensagem_principal() {
let dir = TempDir::new().unwrap();
let arquivo_ruim = dir.path().join("ruim.env");
std::fs::write(&arquivo_ruim, "OUTRA_VAR=valor\n# sem CONTEXT7_API=\n").unwrap();
let saida = cmd_isolado(&dir)
.args(["keys", "import", arquivo_ruim.to_str().unwrap()])
.output()
.unwrap();
assert!(
!saida.status.success(),
"deve falhar com exit != 0 quando arquivo não tem CONTEXT7_API="
);
let stderr = String::from_utf8_lossy(&saida.stderr);
assert!(
stderr.contains("ruim.env"),
"stderr deve conter o caminho do arquivo. stderr='{stderr}'"
);
assert!(
stderr.contains("CONTEXT7_API"),
"stderr deve mencionar CONTEXT7_API. stderr='{stderr}'"
);
let linhas: Vec<&str> = stderr.lines().collect();
let linha_error = linhas.iter().find(|l| l.starts_with("Error:"));
let linha_caused = linhas.iter().find(|l| l.trim().starts_with("Caused by:"));
if let (Some(error), Some(_caused_by)) = (linha_error, linha_caused) {
let conteudo_error = error.trim_start_matches("Error:").trim();
assert!(
!conteudo_error.is_empty(),
"a linha Error: não deve estar vazia. stderr='{stderr}'"
);
}
assert!(
!stderr.contains("thread 'main' panicked"),
"não deve causar panic. stderr='{stderr}'"
);
}
#[test]
fn testa_keys_add_chave_vazia_falha_com_exit1() {
let temp = TempDir::new().unwrap();
cmd_isolado(&temp)
.args(["keys", "add", ""])
.assert()
.failure()
.stderr(predicate::str::contains("empty").or(predicate::str::contains("vazia")));
}
#[test]
fn testa_keys_add_chave_somente_espacos_falha_com_exit1() {
let temp = TempDir::new().unwrap();
cmd_isolado(&temp)
.args(["keys", "add", " "])
.assert()
.failure()
.stderr(predicate::str::contains("empty").or(predicate::str::contains("vazia")));
}
#[test]
fn testa_keys_add_chave_sem_prefixo_exibe_aviso_em_stderr() {
let temp = TempDir::new().unwrap();
cmd_isolado(&temp)
.args(["keys", "add", "invalid-key-without-prefix-1234567890"])
.assert()
.success()
.stderr(predicate::str::contains("Warning").or(predicate::str::contains("Aviso")));
}
#[test]
fn testa_keys_add_chave_valida_prefixo_nao_exibe_aviso() {
let temp = TempDir::new().unwrap();
let saida = cmd_isolado(&temp)
.args(["keys", "add", "ctx7sk-test-valid-key-1234567890abcdef"])
.output()
.unwrap();
assert!(
saida.status.success(),
"keys add com chave válida deve ter exit 0"
);
let stderr = String::from_utf8_lossy(&saida.stderr);
assert!(
!stderr.contains("Warning") && !stderr.contains("Aviso"),
"stderr não deve conter aviso para chave válida, mas continha: '{stderr}'"
);
}
#[test]
fn testa_keys_list_json_com_chave_produz_json_valido() {
let temp = TempDir::new().unwrap();
cmd_isolado(&temp)
.args(["keys", "add", "ctx7sk-test-json-key-1234567890abcdef"])
.assert()
.success();
let saida = cmd_isolado(&temp)
.args(["--json", "keys", "list"])
.output()
.unwrap();
assert!(saida.status.success(), "keys list --json deve ter exit 0");
let stdout = String::from_utf8_lossy(&saida.stdout);
let parsed: serde_json::Value = serde_json::from_str(&stdout)
.unwrap_or_else(|e| panic!("stdout não é JSON válido: {e} — output: '{stdout}'"));
assert!(parsed.is_array(), "esperava array JSON, obteve: {parsed}");
let arr = parsed.as_array().unwrap();
assert_eq!(arr.len(), 1, "esperava 1 chave no array");
assert!(arr[0].get("index").is_some(), "campo 'index' ausente");
assert!(
arr[0].get("masked_key").is_some(),
"campo 'masked_key' ausente"
);
assert!(arr[0].get("added_at").is_some(), "campo 'added_at' ausente");
}
#[test]
fn testa_keys_list_json_sem_chaves_retorna_array_vazio() {
let temp = TempDir::new().unwrap();
let saida = cmd_isolado(&temp)
.args(["--json", "keys", "list"])
.output()
.unwrap();
assert!(
saida.status.success(),
"keys list --json deve ter exit 0 mesmo sem chaves"
);
let stdout = String::from_utf8_lossy(&saida.stdout).trim().to_string();
assert_eq!(
stdout, "[]",
"esperava array JSON vazio, obteve: '{stdout}'"
);
}
#[test]
fn testa_lang_en_keys_list_vazio_exibe_ingles() {
let temp = TempDir::new().unwrap();
cmd_isolado(&temp)
.args(["--lang", "en", "keys", "list"])
.assert()
.success()
.stdout(predicate::str::contains("No key stored"));
}
#[test]
fn testa_lang_pt_keys_list_vazio_exibe_portugues() {
let temp = TempDir::new().unwrap();
cmd_isolado(&temp)
.args(["--lang", "pt", "keys", "list"])
.assert()
.success()
.stdout(predicate::str::contains("Nenhuma chave armazenada"));
}
#[test]
fn testa_keys_list_json_vazio_eh_json_valido() {
let temp = TempDir::new().unwrap();
let saida = cmd_isolado(&temp)
.args(["--json", "keys", "list"])
.output()
.unwrap();
assert!(
saida.status.success(),
"--json keys list deve ter exit 0 sem chaves"
);
let stdout = String::from_utf8_lossy(&saida.stdout);
let parsed: serde_json::Value = serde_json::from_str(stdout.trim())
.unwrap_or_else(|e| panic!("stdout não é JSON válido: {e} — output: '{stdout}'"));
assert!(parsed.is_array(), "esperava array JSON, obteve: {parsed}");
assert_eq!(
parsed.as_array().unwrap().len(),
0,
"array deve estar vazio sem chaves"
);
}
#[test]
fn testa_keys_list_json_com_chave_tem_campos_esperados() {
let temp = TempDir::new().unwrap();
cmd_isolado(&temp)
.args(["keys", "add", "ctx7sk-json-campos-test-12345678"])
.assert()
.success();
let saida = cmd_isolado(&temp)
.args(["--json", "keys", "list"])
.output()
.unwrap();
assert!(saida.status.success(), "--json keys list deve ter exit 0");
let stdout = String::from_utf8_lossy(&saida.stdout);
let parsed: serde_json::Value = serde_json::from_str(stdout.trim())
.unwrap_or_else(|e| panic!("stdout não é JSON válido: {e} — output: '{stdout}'"));
assert!(parsed.is_array(), "esperava array JSON");
let arr = parsed.as_array().unwrap();
assert_eq!(arr.len(), 1, "esperava 1 elemento no array");
let obj = &arr[0];
assert!(
obj.get("index").is_some(),
"campo 'index' deve estar presente"
);
assert!(
obj.get("masked_key").is_some(),
"campo 'masked_key' deve estar presente"
);
assert!(
obj.get("added_at").is_some(),
"campo 'added_at' deve estar presente"
);
}
#[test]
fn testa_keys_list_json_added_at_formato_legivel() {
let temp = TempDir::new().unwrap();
cmd_isolado(&temp)
.args(["keys", "add", "ctx7sk-added-at-format-test-12345"])
.assert()
.success();
let saida = cmd_isolado(&temp)
.args(["--json", "keys", "list"])
.output()
.unwrap();
assert!(saida.status.success(), "--json keys list deve ter exit 0");
let stdout = String::from_utf8_lossy(&saida.stdout);
let parsed: serde_json::Value = serde_json::from_str(stdout.trim())
.unwrap_or_else(|e| panic!("stdout não é JSON válido: {e} — output: '{stdout}'"));
let arr = parsed.as_array().unwrap();
assert!(!arr.is_empty(), "array não deve estar vazio");
let added_at = arr[0]["added_at"].as_str().unwrap_or("");
assert!(!added_at.is_empty(), "campo added_at não deve ser vazio");
assert!(
added_at.contains(' '),
"added_at deve conter espaço (formato YYYY-MM-DD HH:MM:SS), obteve: '{added_at}'"
);
assert!(
!added_at.contains('T'),
"added_at não deve conter 'T' (não deve ser RFC3339), obteve: '{added_at}'"
);
}
#[test]
#[serial]
#[allow(deprecated)] fn testa_context7_home_rejeita_path_traversal() {
use assert_cmd::Command as Cmd;
let dir = TempDir::new().unwrap();
let saida = Cmd::cargo_bin("context7")
.unwrap()
.env_clear()
.env("CONTEXT7_HOME", "../../../etc")
.env("HOME", dir.path())
.args(["keys", "path"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&saida.stdout);
assert!(
!stdout.contains(".."),
"keys path não deve retornar caminho com '..' quando CONTEXT7_HOME contém path traversal. stdout='{stdout}'"
);
}
#[test]
fn testa_output_portugues_contem_acentuacao() {
let temp = TempDir::new().unwrap();
let saida = cmd_isolado(&temp)
.args(["--lang", "pt", "keys", "list"])
.output()
.unwrap();
assert!(
saida.status.success(),
"--lang pt keys list deve ter exit 0"
);
let stdout = String::from_utf8(saida.stdout).expect("stdout deve ser UTF-8 válido");
assert!(
stdout.contains("Nenhuma"),
"output em PT deve conter 'Nenhuma' (com N maiúsculo e caractere 'e'). stdout='{stdout}'"
);
}
#[test]
fn testa_completions_bash_gera_saida() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.args(["completions", "bash"])
.assert()
.success()
.stdout(predicate::str::contains("context7"))
.stdout(predicate::str::is_empty().not());
}
#[test]
fn testa_completions_zsh_gera_saida() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.args(["completions", "zsh"])
.assert()
.success()
.stdout(predicate::str::is_empty().not());
}
#[test]
fn testa_completions_fish_gera_saida() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.args(["completions", "fish"])
.assert()
.success()
.stdout(predicate::str::is_empty().not());
}
#[test]
fn testa_completions_powershell_gera_saida() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.args(["completions", "powershell"])
.assert()
.success()
.stdout(predicate::str::is_empty().not());
}
#[test]
fn testa_completions_alias_completion_funciona() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.args(["completion", "bash"])
.assert()
.success()
.stdout(predicate::str::contains("context7"));
}
#[test]
fn testa_completions_help_lista_shells() {
let dir = TempDir::new().unwrap();
let saida = cmd_isolado(&dir)
.args(["completions", "--help"])
.output()
.unwrap();
assert!(saida.status.success(), "completions --help deve ter exit 0");
let stdout = String::from_utf8_lossy(&saida.stdout);
let stderr = String::from_utf8_lossy(&saida.stderr);
let combinado = format!("{stdout}{stderr}");
assert!(
combinado.contains("bash")
|| combinado.contains("zsh")
|| combinado.contains("fish")
|| combinado.contains("shell"),
"completions --help deve mencionar shells disponíveis. combinado='{combinado}'"
);
}
#[test]
fn testa_completions_nao_requer_api_key() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.args(["completions", "bash"])
.assert()
.success();
}
#[test]
fn testa_user_agent_nao_contem_versao_hardcoded() {
let conteudo_api = include_str!("../src/api.rs");
let linhas_user_agent: Vec<&str> = conteudo_api
.lines()
.filter(|l| l.contains(".user_agent("))
.collect();
assert!(
!linhas_user_agent.is_empty(),
"src/api.rs deve conter pelo menos uma chamada a .user_agent()"
);
for linha in &linhas_user_agent {
assert!(
!linha.contains("context7-cli/0."),
"src/api.rs:.user_agent() contém versão hardcoded (deve usar env!(\"CARGO_PKG_VERSION\")). linha='{linha}'"
);
assert!(
linha.contains("CARGO_PKG_VERSION"),
"src/api.rs:.user_agent() deve usar env!(\"CARGO_PKG_VERSION\"). linha='{linha}'"
);
}
}