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 test_help_renders_without_panic() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("Usage").or(predicate::str::contains("usage")));
}
#[test]
fn test_version_does_not_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 test_help_subcommand_library() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.args(["library", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("Usage").or(predicate::str::contains("name")));
}
#[test]
fn test_help_subcommand_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 test_help_subcommand_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 test_library_subcommand_without_keys_returns_user_friendly_error() {
let dir = TempDir::new().unwrap();
let saida = cmd_isolado(&dir)
.args(["library", "react"])
.output()
.unwrap();
assert!(!saida.status.success(), "esperava failure sem key 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()"),
"must not leak unwrap message: {saida_combinada}"
);
}
#[test]
#[serial]
fn test_docs_subcommand_without_keys_returns_user_friendly_error() {
let dir = TempDir::new().unwrap();
let saida = cmd_isolado(&dir)
.args(["docs", "/facebook/react"])
.output()
.unwrap();
assert!(!saida.status.success(), "esperava failure sem key 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 test_keys_list_subcommand_empty_returns_appropriate_message() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.args(["keys", "list"])
.assert()
.success()
.stdout(
predicate::str::contains("Nenhuma key")
.or(predicate::str::contains("0 key"))
.or(predicate::str::contains("No key"))
.or(predicate::str::contains("No keys")),
);
}
#[test]
#[serial]
fn test_keys_path_subcommand_returns_xdg_path() {
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 test_keys_add_subcommand_success() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.args(["keys", "add", "ctx7sk-teste-key-123456789012"])
.assert()
.success();
}
#[test]
#[serial]
fn test_keys_add_then_list_shows_masked_key() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.args(["keys", "add", "ctx7sk-key-integracao-12345678"])
.assert()
.success();
cmd_isolado(&dir)
.args(["keys", "list"])
.assert()
.success()
.stdout(predicate::str::contains("1 key").or(predicate::str::contains("[1]")));
}
#[test]
#[serial]
fn test_keys_remove_invalid_index_returns_user_friendly_message() {
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 with invalid index must not panic"
);
}
#[test]
#[serial]
fn test_keys_clear_yes_success() {
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 key")
.or(predicate::str::contains("0 key"))
.or(predicate::str::contains("No key"))
.or(predicate::str::contains("No keys")),
);
}
#[test]
#[serial]
fn test_keys_export_no_keys_empty_output() {
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 test_keys_export_with_key_correct_format() {
let dir = TempDir::new().unwrap();
let key = "ctx7sk-export-teste-123456789012";
cmd_isolado(&dir)
.args(["keys", "add", key])
.assert()
.success();
cmd_isolado(&dir)
.args(["keys", "export"])
.assert()
.success()
.stdout(predicate::str::contains(format!("CONTEXT7_API={key}")));
}
#[test]
fn test_alias_lib_equivalent_to_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 test_alias_doc_equivalent_to_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 test_alias_key_equivalent_to_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 test_invalid_subcommand_returns_nonzero_exit_code() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.arg("subcomando-que-nao-existe")
.assert()
.failure();
}
#[test]
#[serial]
fn test_json_flag_with_keys_list_does_not_crash() {
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 test_docs_without_keys_shows_error_without_panic() {
let dir = TempDir::new().unwrap();
let saida = cmd_isolado(&dir)
.args(["docs", "/test/lib"])
.output()
.unwrap();
assert!(!saida.status.success(), "docs sem keys deve falhar");
let stderr = String::from_utf8_lossy(&saida.stderr);
let stdout = String::from_utf8_lossy(&saida.stdout);
let combined = format!("{stdout}{stderr}");
assert!(
!combined.contains("thread 'main' panicked"),
"docs sem keys não deve causar panic: {combined}"
);
assert!(
!combined.contains("unwrap()"),
"docs without keys must not leak unwrap message: {combined}"
);
}
#[test]
fn test_docs_help_renders() {
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 test_keys_rotate_returns_unrecognised_subcommand_error() {
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 must mention invalid subcommand: {stderr}"
);
assert!(
!stderr.contains("thread 'main' panicked"),
"keys rotate não deve causar panic: {stderr}"
);
}
#[test]
fn test_library_help_shows_name_in_english() {
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 test_keys_add_help_shows_key_in_english() {
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 test_keys_remove_help_shows_index_in_english() {
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 test_keys_import_help_shows_file_in_english() {
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 test_library_help_does_not_contain_effect_hooks() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.args(["library", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("hooks de efeito").not());
}
#[test]
fn test_library_help_contains_example_in_english() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.args(["library", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("effect hooks"));
}
#[test]
fn test_library_not_found_hint_message_contains_library_command() {
use context7_cli::i18n::{Language, Message};
let en = Message::LibraryNotFoundApi.text(Language::English);
let pt = Message::LibraryNotFoundApi.text(Language::Portuguese);
assert!(
en.to_lowercase().contains("library"),
"EN hint must mention 'library': {en}"
);
assert!(
pt.to_lowercase().contains("library"),
"PT hint must mention 'library': {pt}"
);
}
#[test]
#[serial]
fn test_docs_without_keys_does_not_expose_internal_retry_details() {
let dir = TempDir::new().unwrap();
let saida = cmd_isolado(&dir)
.args(["docs", "/biblioteca/inexistente"])
.output()
.unwrap();
assert!(!saida.status.success(), "docs sem keys deve falhar");
let stderr = String::from_utf8_lossy(&saida.stderr);
let stdout = String::from_utf8_lossy(&saida.stdout);
let combined = format!("{stdout}{stderr}");
assert!(
!combined.contains("No valid API key after"),
"message não deve expor mecanismo de retry: {combined}"
);
assert!(
!combined.contains("thread 'main' panicked"),
"must not cause panic: {combined}"
);
}
#[test]
fn test_docs_help_shows_alias_q_and_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 test_docs_alias_q_accepted_as_valid_argument() {
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 test_keys_import_nonexistent_file_helpful_message() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.args(["keys", "import", "/tmp/file-que-nao-existe-v026.env"])
.assert()
.failure()
.stderr(
predicate::str::contains("file")
.or(predicate::str::contains("file"))
.or(predicate::str::contains("No such")),
);
}
#[test]
fn test_keys_import_file_without_context7_keys_helpful_message() {
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=value\n",
)
.unwrap();
cmd_isolado(&dir)
.args(["keys", "import", arquivo_invalido.to_str().unwrap()])
.assert()
.failure()
.stderr(predicate::str::contains("CONTEXT7_API"));
}
#[test]
#[serial]
fn test_keys_add_duplicate_shows_existing_key_warning() {
let dir = TempDir::new().unwrap();
let key = "ctx7sk-dedup-regression-v026-abc";
cmd_isolado(&dir)
.args(["keys", "add", key])
.assert()
.success();
let saida = cmd_isolado(&dir)
.args(["keys", "add", key])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&saida.stdout);
let stderr = String::from_utf8_lossy(&saida.stderr);
let combined = format!("{stdout}{stderr}");
assert!(
combined.to_lowercase().contains("existe")
|| combined.to_lowercase().contains("exists")
|| combined.to_lowercase().contains("already")
|| combined.to_lowercase().contains("ignor")
|| combined.to_lowercase().contains("skip"),
"segunda adição deve exibir aviso de duplicata. stdout='{stdout}' stderr='{stderr}'"
);
assert!(
!combined.contains("thread 'main' panicked"),
"must not cause panic: {combined}"
);
}
#[test]
#[serial]
fn test_regression_qa028_fail1_import_no_keys_error_contains_path() {
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 name do file. stderr='{stderr}'"
);
assert!(
stderr.contains("CONTEXT7_API"),
"stderr must mention CONTEXT7_API. stderr='{stderr}'"
);
assert!(
!stderr.contains("thread 'main' panicked"),
"não deve causar panic. stderr='{stderr}'"
);
}
#[test]
#[serial]
fn test_regression_qa028_fail1_import_only_comments_fails_with_exit1() {
let dir = TempDir::new().unwrap();
let arquivo_comentarios = dir.path().join("comentarios.env");
std::fs::write(
&arquivo_comentarios,
"# Este file é 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 test_regression_qa028_fail3_short_key_shows_asterisks() {
let dir = TempDir::new().unwrap();
let short_key = "ctx7sk-ab";
cmd_isolado(&dir)
.args(["keys", "add", short_key])
.assert()
.success();
let saida = cmd_isolado(&dir).args(["keys", "list"]).output().unwrap();
let stdout = String::from_utf8_lossy(&saida.stdout);
assert!(
!stdout.contains(short_key),
"key curta não deve aparecer sem mascaramento. stdout='{stdout}'"
);
assert!(
stdout.contains("***"),
"key curta deve ser exibida como '***'. stdout='{stdout}'"
);
}
#[test]
#[allow(deprecated)] #[serial]
fn test_regression_qa028_partial1_context7_home_path_traversal_uses_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 must not appear in the output. 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 test_regression_v040_version_string_correct() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.arg("--version")
.assert()
.success()
.stdout(predicate::str::contains(env!("CARGO_PKG_VERSION")));
}
#[test]
#[serial]
fn test_regression_v028_fail1_import_error_does_not_duplicate_main_message() {
let dir = TempDir::new().unwrap();
let arquivo_ruim = dir.path().join("ruim.env");
std::fs::write(&arquivo_ruim, "OUTRA_VAR=value\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 file não tem CONTEXT7_API="
);
let stderr = String::from_utf8_lossy(&saida.stderr);
assert!(
stderr.contains("ruim.env"),
"stderr deve conter o path do file. stderr='{stderr}'"
);
assert!(
stderr.contains("CONTEXT7_API"),
"stderr must mention 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 line Error: não deve estar vazia. stderr='{stderr}'"
);
}
assert!(
!stderr.contains("thread 'main' panicked"),
"não deve causar panic. stderr='{stderr}'"
);
}
#[test]
fn test_keys_add_empty_key_fails_with_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 test_keys_add_whitespace_only_key_fails_with_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 test_keys_add_key_without_prefix_shows_warning_on_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 test_keys_add_valid_prefix_key_does_not_show_warning() {
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 key 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 key válida, mas continha: '{stderr}'"
);
}
#[test]
fn test_keys_list_json_with_key_produces_valid_json() {
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 key 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 test_keys_list_json_without_keys_returns_empty_array() {
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 keys"
);
let stdout = String::from_utf8_lossy(&saida.stdout).trim().to_string();
assert_eq!(
stdout, "[]",
"esperava array JSON vazio, obteve: '{stdout}'"
);
}
#[test]
fn test_lang_en_keys_list_empty_shows_english() {
let temp = TempDir::new().unwrap();
cmd_isolado(&temp)
.args(["--lang", "en", "keys", "list"])
.assert()
.success()
.stdout(predicate::str::contains("No key stored"));
}
#[test]
fn test_lang_pt_keys_list_empty_shows_portuguese() {
let temp = TempDir::new().unwrap();
cmd_isolado(&temp)
.args(["--lang", "pt", "keys", "list"])
.assert()
.success()
.stdout(predicate::str::contains("Nenhuma chave armazenada"));
}
#[test]
fn test_keys_list_json_empty_is_valid_json() {
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 keys"
);
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 keys"
);
}
#[test]
fn test_keys_list_json_with_key_has_expected_fields() {
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 test_keys_list_json_added_at_legible_format() {
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 test_context7_home_rejects_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 path com '..' quando CONTEXT7_HOME contém path traversal. stdout='{stdout}'"
);
}
#[test]
fn test_output_portuguese_contains_accents() {
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 character 'e'). stdout='{stdout}'"
);
}
#[test]
fn test_completions_bash_generates_output() {
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 test_completions_zsh_generates_output() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.args(["completions", "zsh"])
.assert()
.success()
.stdout(predicate::str::is_empty().not());
}
#[test]
fn test_completions_fish_generates_output() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.args(["completions", "fish"])
.assert()
.success()
.stdout(predicate::str::is_empty().not());
}
#[test]
fn test_completions_powershell_generates_output() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.args(["completions", "powershell"])
.assert()
.success()
.stdout(predicate::str::is_empty().not());
}
#[test]
fn test_completions_alias_completion_works() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.args(["completion", "bash"])
.assert()
.success()
.stdout(predicate::str::contains("context7"));
}
#[test]
fn test_completions_help_lists_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 combined = format!("{stdout}{stderr}");
assert!(
combined.contains("bash")
|| combined.contains("zsh")
|| combined.contains("fish")
|| combined.contains("shell"),
"completions --help must mention available shells. combined='{combined}'"
);
}
#[test]
fn test_completions_does_not_require_api_key() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.args(["completions", "bash"])
.assert()
.success();
}
#[test]
fn test_user_agent_does_not_contain_hardcoded_version() {
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 line in &linhas_user_agent {
assert!(
!line.contains("context7-cli/0."),
"src/api.rs:.user_agent() contém versão hardcoded (deve usar env!(\"CARGO_PKG_VERSION\")). line='{line}'"
);
assert!(
line.contains("CARGO_PKG_VERSION"),
"src/api.rs:.user_agent() deve usar env!(\"CARGO_PKG_VERSION\"). line='{line}'"
);
}
}
#[test]
#[serial]
fn test_quiet_suppresses_stdout_in_empty_keys_list() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.arg("--quiet")
.args(["keys", "list"])
.assert()
.success()
.stdout(predicate::str::is_empty());
}
#[test]
#[serial]
fn test_quiet_preserves_stderr_in_no_key_error() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.arg("--quiet")
.args(["library", "react"])
.assert()
.failure()
.stdout(predicate::str::is_empty())
.stderr(predicate::str::is_empty().not());
}
#[test]
#[serial]
fn test_no_quiet_produces_stdout_in_empty_keys_list() {
let dir = TempDir::new().unwrap();
cmd_isolado(&dir)
.args(["keys", "list"])
.assert()
.success()
.stdout(predicate::str::is_empty().not());
}