use assert_cmd::Command;
use predicates::prelude::*;
use serial_test::serial;
use tempfile::TempDir;
#[allow(deprecated)] fn cmd_idioma(dir: &TempDir) -> Command {
let mut cmd = Command::cargo_bin("context7").unwrap();
cmd.env_clear()
.env("CONTEXT7_HOME", dir.path())
.env("HOME", dir.path());
cmd
}
#[test]
#[serial]
fn test_help_renders_without_panic_independent_of_lang() {
let dir = TempDir::new().unwrap();
cmd_idioma(&dir).arg("--help").assert().success();
}
#[test]
#[serial]
fn test_keys_list_empty_message_in_portuguese() {
let dir = TempDir::new().unwrap();
cmd_idioma(&dir)
.env("CONTEXT7_LANG", "pt")
.args(["keys", "list"])
.assert()
.success()
.stdout(
predicate::str::contains("Nenhuma key")
.or(predicate::str::contains("nenhuma"))
.or(predicate::str::contains("Use"))
.or(predicate::str::contains("0 key")),
);
}
#[test]
#[serial]
fn test_keys_list_empty_does_not_show_system_error() {
let dir = TempDir::new().unwrap();
let saida = cmd_idioma(&dir).args(["keys", "list"]).output().unwrap();
assert!(
saida.status.success(),
"keys list without keys must return exit 0"
);
let stderr = String::from_utf8_lossy(&saida.stderr);
assert!(
!stderr.contains("Error") || stderr.is_empty(),
"keys list must not produce error messages on stderr: {stderr}"
);
}
#[test]
#[serial]
fn test_env_context7_lang_pt_accepted_without_crash() {
let dir = TempDir::new().unwrap();
cmd_idioma(&dir)
.env("CONTEXT7_LANG", "pt")
.args(["keys", "list"])
.assert()
.success();
}
#[test]
#[serial]
fn test_env_context7_lang_en_accepted_without_crash() {
let dir = TempDir::new().unwrap();
cmd_idioma(&dir)
.env("CONTEXT7_LANG", "en")
.args(["keys", "list"])
.assert()
.success();
}
#[test]
#[serial]
fn test_env_context7_lang_invalid_uses_fallback_without_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"),
"Invalid CONTEXT7_LANG must not cause panic: {stderr}"
);
}
#[test]
#[serial]
fn test_no_key_error_legible_message_pt() {
let dir = TempDir::new().unwrap();
let saida = cmd_idioma(&dir)
.env("CONTEXT7_LANG", "pt")
.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 combined = format!("{stdout}{stderr}");
assert!(
!combined.contains("thread 'main' panicked"),
"must not panic: {combined}"
);
}
#[test]
#[serial]
fn test_no_key_error_legible_message_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 combined = format!("{stdout}{stderr}");
assert!(
!combined.contains("thread 'main' panicked"),
"must not panic: {combined}"
);
}
#[test]
#[serial]
fn test_b2_missing_key_in_portuguese_uses_pt_message() {
let dir = TempDir::new().unwrap();
let saida = cmd_idioma(&dir)
.env("CONTEXT7_LANG", "pt")
.args(["library", "react"])
.output()
.unwrap();
assert!(
!saida.status.success(),
"Deve falhar sem key de API configurada"
);
let stderr = String::from_utf8_lossy(&saida.stderr);
let stdout = String::from_utf8_lossy(&saida.stdout);
let combined = format!("{stdout}{stderr}");
assert!(
combined.contains("key de API") || combined.contains("CONTEXT7_API_KEYS"),
"PT message must mention 'key de API' or 'CONTEXT7_API_KEYS', got: {combined}"
);
assert!(
!combined.contains("thread 'main' panicked"),
"Must not cause panic: {combined}"
);
}
#[test]
#[serial]
fn test_b2_missing_key_in_english_uses_en_message() {
let dir = TempDir::new().unwrap();
let saida = cmd_idioma(&dir)
.env("CONTEXT7_LANG", "en")
.args(["library", "react"])
.output()
.unwrap();
assert!(
!saida.status.success(),
"Deve falhar sem key de API configurada"
);
let stderr = String::from_utf8_lossy(&saida.stderr);
let stdout = String::from_utf8_lossy(&saida.stdout);
let combined = format!("{stdout}{stderr}");
assert!(
combined.contains("API key") || combined.contains("CONTEXT7_API_KEYS"),
"EN message must mention 'API key' or 'CONTEXT7_API_KEYS', got: {combined}"
);
assert!(
!combined.contains("thread 'main' panicked"),
"Must not cause panic: {combined}"
);
}
#[test]
#[serial]
fn test_keys_list_masks_long_key() {
let dir = TempDir::new().unwrap();
let key = "ctx7sk-key-muito-longa-para-mascarar";
cmd_idioma(&dir)
.args(["keys", "add", key])
.assert()
.success();
let output = cmd_idioma(&dir).args(["keys", "list"]).output().unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout.contains(key),
"full key must not appear in clear text in the list: {stdout}"
);
assert!(
stdout.contains("..."),
"long masked key must use '...': {stdout}"
);
}
#[test]
#[serial]
fn test_keys_list_masks_short_key() {
let dir = TempDir::new().unwrap();
let key = "abc1234"; cmd_idioma(&dir)
.args(["keys", "add", key])
.assert()
.success();
let output = cmd_idioma(&dir).args(["keys", "list"]).output().unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout.contains(key),
"full short key must not appear in the list: {stdout}"
);
}
use context7_cli::i18n::{resolve_language, Language, Message};
#[test]
fn test_resolve_explicit_flag_en() {
let language = resolve_language(Some("en"));
assert!(matches!(language, Language::English));
}
#[test]
fn test_resolve_explicit_flag_pt() {
let language = resolve_language(Some("pt"));
assert!(matches!(language, Language::Portuguese));
}
#[test]
#[serial]
fn test_resolve_env_context7_lang_pt_when_no_flag() {
unsafe { std::env::set_var("CONTEXT7_LANG", "pt") };
let language = resolve_language(None);
unsafe { std::env::remove_var("CONTEXT7_LANG") };
assert!(matches!(language, Language::Portuguese));
}
#[test]
fn test_resolve_fallback_when_all_none_returns_valid_language() {
let language = resolve_language(None);
assert!(matches!(language, Language::English | Language::Portuguese));
}
#[test]
fn test_operation_cancelled_message_en_vs_pt_are_distinct() {
let variante = Message::OperationCancelled;
let _ = variante; let _ = resolve_language(Some("en"));
let _ = resolve_language(Some("pt"));
}
use context7_cli::i18n::t;
#[test]
fn test_fetch_documentation_failure_message_accessible_and_not_empty() {
let text = t(Message::DocsFetchFailure);
assert!(
!text.is_empty(),
"DocsFetchFailure must be non-empty"
);
let texto_lower = text.to_lowercase();
assert!(
texto_lower.contains("doc")
|| texto_lower.contains("fetch")
|| texto_lower.contains("failure")
|| texto_lower.contains("failed"),
"DocsFetchFailure must mention doc/fetch/failure/failed: {text}"
);
}
#[test]
fn test_search_library_failure_message_accessible_and_not_empty() {
let text = t(Message::LibrarySearchFailure);
assert!(
!text.is_empty(),
"LibrarySearchFailure must be non-empty"
);
}
#[test]
fn test_create_http_client_failure_message_accessible_and_not_empty() {
let text = t(Message::HttpClientCreateFailure);
assert!(
!text.is_empty(),
"HttpClientCreateFailure must be non-empty"
);
}
#[test]
fn test_serialise_json_failure_message_accessible_and_not_empty() {
let text = t(Message::JsonSerialiseFailure);
assert!(!text.is_empty(), "JsonSerialiseFailure must be non-empty");
}
#[test]
fn test_no_documentation_available_message_accessible_and_not_empty() {
let text = t(Message::NoDocumentationAvailable);
assert!(
!text.is_empty(),
"NoDocumentationAvailable must be non-empty"
);
}
#[test]
fn test_library_not_found_api_message_en_pt() {
let en = Message::LibraryNotFoundApi.text(Language::English);
let pt = Message::LibraryNotFoundApi.text(Language::Portuguese);
assert!(
!en.is_empty(),
"EN LibraryNotFoundApi must not be empty"
);
assert!(
!pt.is_empty(),
"PT LibraryNotFoundApi must not be empty"
);
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 must mention 'library' or 'not found', got: {en}"
);
assert!(
pt.to_lowercase().contains("biblioteca") || pt.to_lowercase().contains("encontrada"),
"PT must mention 'biblioteca' or 'encontrada', got: {pt}"
);
}
#[test]
fn test_trust_score_en_is_lowercase_without_colon_v023() {
let en = Message::TrustScore.text(Language::English);
assert_eq!(
en, "trust",
"TrustScore EN deve ser 'trust' em v0.2.3, obteve: {en}"
);
}
#[test]
fn test_trust_score_pt_is_lowercase_without_colon_v023() {
let pt = Message::TrustScore.text(Language::Portuguese);
assert_eq!(
pt, "confiança",
"TrustScore PT deve ser 'confiança' em v0.2.3, obteve: {pt}"
);
}
#[test]
#[serial]
fn test_keys_add_message_en() {
let dir = TempDir::new().unwrap();
cmd_idioma(&dir)
.args([
"--lang",
"en",
"keys",
"add",
"ctx7sk-i18n-en-add-test-00001",
])
.assert()
.success()
.stdout(
predicate::str::contains("added")
.or(predicate::str::contains("Key added"))
.or(predicate::str::contains("successfully")),
);
}
#[test]
#[serial]
fn test_keys_add_message_pt() {
let dir = TempDir::new().unwrap();
cmd_idioma(&dir)
.args([
"--lang",
"pt",
"keys",
"add",
"ctx7sk-i18n-pt-add-test-00002",
])
.assert()
.success()
.stdout(
predicate::str::contains("adicionada").or(predicate::str::contains("Chave adicionada")),
);
}
#[test]
#[serial]
fn test_keys_remove_message_en() {
let dir = TempDir::new().unwrap();
cmd_idioma(&dir)
.args(["keys", "add", "ctx7sk-i18n-en-remove-test-00003"])
.assert()
.success();
cmd_idioma(&dir)
.args(["--lang", "en", "keys", "remove", "1"])
.assert()
.success()
.stdout(predicate::str::contains("removed").or(predicate::str::contains("Key removed")));
}
#[test]
#[serial]
fn test_keys_remove_message_pt() {
let dir = TempDir::new().unwrap();
cmd_idioma(&dir)
.args(["keys", "add", "ctx7sk-i18n-pt-remove-test-00004"])
.assert()
.success();
cmd_idioma(&dir)
.args(["--lang", "pt", "keys", "remove", "1"])
.assert()
.success()
.stdout(
predicate::str::contains("removed").or(predicate::str::contains("Chave removida")),
);
}
#[test]
#[serial]
fn test_keys_clear_message_en() {
let dir = TempDir::new().unwrap();
cmd_idioma(&dir)
.args(["keys", "add", "ctx7sk-i18n-en-clear-test-00005"])
.assert()
.success();
cmd_idioma(&dir)
.args(["--lang", "en", "keys", "clear", "--yes"])
.assert()
.success()
.stdout(
predicate::str::contains("removed")
.or(predicate::str::contains("keys removed"))
.or(predicate::str::contains("All keys")),
);
}
#[test]
#[serial]
fn test_keys_clear_message_pt() {
let dir = TempDir::new().unwrap();
cmd_idioma(&dir)
.args(["keys", "add", "ctx7sk-i18n-pt-clear-test-00006"])
.assert()
.success();
cmd_idioma(&dir)
.args(["--lang", "pt", "keys", "clear", "--yes"])
.assert()
.success()
.stdout(
predicate::str::contains("removidas").or(predicate::str::contains("Todas as keys")),
);
}
#[test]
#[serial]
fn test_no_key_error_en() {
let dir = TempDir::new().unwrap();
let saida = cmd_idioma(&dir)
.args(["--lang", "en", "library", "react"])
.output()
.unwrap();
assert!(!saida.status.success(), "deve falhar sem key de API");
let stderr = String::from_utf8_lossy(&saida.stderr);
let stdout = String::from_utf8_lossy(&saida.stdout);
let combined = format!("{stdout}{stderr}");
assert!(
combined.to_lowercase().contains("no api key")
|| combined.to_lowercase().contains("api key")
|| combined.contains("No API key")
|| combined.contains("CONTEXT7_API_KEYS"),
"EN error must mention 'API key' or 'CONTEXT7_API_KEYS', got: {combined}"
);
assert!(
!combined.contains("thread 'main' panicked"),
"must not cause panic: {combined}"
);
}
#[test]
#[serial]
fn test_no_key_error_pt() {
let dir = TempDir::new().unwrap();
let saida = cmd_idioma(&dir)
.args(["--lang", "pt", "library", "react"])
.output()
.unwrap();
assert!(!saida.status.success(), "deve falhar sem key de API");
let stderr = String::from_utf8_lossy(&saida.stderr);
let stdout = String::from_utf8_lossy(&saida.stdout);
let combined = format!("{stdout}{stderr}");
assert!(
combined.contains("Nenhuma")
|| combined.contains("key de API")
|| combined.contains("CONTEXT7_API_KEYS"),
"PT error must mention 'Nenhuma' or 'key de API', got: {combined}"
);
assert!(
!combined.contains("thread 'main' panicked"),
"must not cause panic: {combined}"
);
}