use assert_cmd::Command;
use predicates::prelude::*;
use serial_test::serial;
use tempfile::TempDir;
fn cmd_xdg(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
}
#[test]
#[serial]
fn testa_add_list_remove_ciclo_completo_via_xdg_home() {
let dir = TempDir::new().unwrap();
let chave = "ctx7sk-ciclo-completo-12345678901";
cmd_xdg(&dir)
.args(["keys", "add", chave])
.assert()
.success();
cmd_xdg(&dir)
.args(["keys", "list"])
.assert()
.success()
.stdout(predicate::str::contains("[1]").or(predicate::str::contains("1 chave")));
cmd_xdg(&dir)
.args(["keys", "remove", "1"])
.assert()
.success();
cmd_xdg(&dir)
.args(["keys", "list"])
.assert()
.success()
.stdout(
predicate::str::contains("Nenhuma chave")
.or(predicate::str::contains("No key"))
.or(predicate::str::contains("0 chave")),
);
}
#[test]
#[serial]
fn testa_import_export_roundtrip() {
let dir = TempDir::new().unwrap();
let chave1 = "ctx7sk-import-chave-aaa-1234567890";
let chave2 = "ctx7sk-import-chave-bbb-1234567890";
let env_file = dir.path().join("test.env");
std::fs::write(
&env_file,
format!("CONTEXT7_API={chave1}\nCONTEXT7_API={chave2}\n"),
)
.unwrap();
cmd_xdg(&dir)
.args(["keys", "import", env_file.to_str().unwrap()])
.assert()
.success();
cmd_xdg(&dir)
.args(["keys", "export"])
.assert()
.success()
.stdout(predicate::str::contains(format!("CONTEXT7_API={chave1}")))
.stdout(predicate::str::contains(format!("CONTEXT7_API={chave2}")));
}
#[test]
#[serial]
fn testa_clear_remove_todas_as_chaves() {
let dir = TempDir::new().unwrap();
for i in 1..=3 {
cmd_xdg(&dir)
.args([
"keys",
"add",
&format!("ctx7sk-chave-clear-{i:02}-1234567890"),
])
.assert()
.success();
}
cmd_xdg(&dir)
.args(["keys", "clear", "--yes"])
.assert()
.success();
cmd_xdg(&dir)
.args(["keys", "list"])
.assert()
.success()
.stdout(
predicate::str::contains("Nenhuma chave")
.or(predicate::str::contains("No key"))
.or(predicate::str::contains("0 chave")),
);
}
#[test]
#[serial]
fn testa_path_retorna_xdg_config_home_override() {
let dir = TempDir::new().unwrap();
let dir_str = dir.path().to_str().unwrap().to_owned();
cmd_xdg(&dir)
.args(["keys", "path"])
.assert()
.success()
.stdout(predicate::str::contains(&dir_str).or(predicate::str::contains("context7")));
}
#[test]
#[serial]
fn testa_add_chaves_duplicadas_nao_acumula() {
let dir = TempDir::new().unwrap();
let chave = "ctx7sk-duplicada-12345678901234";
cmd_xdg(&dir)
.args(["keys", "add", chave])
.assert()
.success();
cmd_xdg(&dir)
.args(["keys", "add", chave])
.assert()
.success();
let output = cmd_xdg(&dir).args(["keys", "export"]).output().unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let ocorrencias = stdout
.lines()
.filter(|l| l.contains(&format!("CONTEXT7_API={chave}")))
.count();
assert_eq!(
ocorrencias, 1,
"chave duplicada não deve ser armazenada duas vezes"
);
}
#[test]
#[serial]
fn testa_remove_indice_invalido_retorna_erro_controlado() {
let dir = TempDir::new().unwrap();
let saida = cmd_xdg(&dir)
.args(["keys", "remove", "99"])
.output()
.unwrap();
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"),
"remove com índice inválido não deve panic: {combinado}"
);
assert!(
!combinado.contains("index out of bounds"),
"não deve vazar mensagem interna de bounds: {combinado}"
);
}
#[test]
#[serial]
fn testa_remove_chave_do_meio_preserva_demais() {
let dir = TempDir::new().unwrap();
let chave1 = "ctx7sk-chave-primeira-1234567890";
let chave2 = "ctx7sk-chave-segunda--1234567890";
let chave3 = "ctx7sk-chave-terceira-1234567890";
for chave in [chave1, chave2, chave3] {
cmd_xdg(&dir)
.args(["keys", "add", chave])
.assert()
.success();
}
cmd_xdg(&dir)
.args(["keys", "remove", "2"])
.assert()
.success();
let output = cmd_xdg(&dir).args(["keys", "export"]).output().unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains(chave1),
"chave1 deve persistir após remover chave2"
);
assert!(!stdout.contains(chave2), "chave2 deve ter sido removida");
assert!(
stdout.contains(chave3),
"chave3 deve persistir após remover chave2"
);
}
#[test]
#[serial]
#[cfg(unix)]
fn testa_config_toml_tem_permissoes_600_em_unix() {
use std::os::unix::fs::PermissionsExt;
let dir = TempDir::new().unwrap();
cmd_xdg(&dir)
.args(["keys", "add", "ctx7sk-permissao-unix-12345678"])
.assert()
.success();
let config_path = dir.path().join("context7").join("config.toml");
if config_path.exists() {
let metadata = std::fs::metadata(&config_path).unwrap();
let modo = metadata.permissions().mode();
let bits_outros = modo & 0o077;
assert_eq!(
bits_outros, 0,
"config.toml deve ter permissões 600, outros bits: {bits_outros:o}"
);
}
}