use anyhow::{bail, Context, Result};
use chrono::Utc;
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use zeroize::{Zeroize, ZeroizeOnDrop};
use unicode_normalization::UnicodeNormalization;
use crate::errors::ErroContext7;
use crate::i18n::{t, Mensagem};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ChaveArmazenada {
pub value: String,
pub added_at: String,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct ConfigArquivo {
pub schema_version: u32,
#[serde(default)]
pub keys: Vec<ChaveArmazenada>,
}
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub struct ChaveApi(String);
impl ChaveApi {
pub fn new(valor: String) -> Self {
Self(valor)
}
pub fn valor(&self) -> &str {
&self.0
}
}
impl PartialEq<&str> for ChaveApi {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
impl std::fmt::Debug for ChaveApi {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ChaveApi({})", mascarar_chave(self.valor()))
}
}
impl std::fmt::Display for ChaveApi {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", mascarar_chave(self.valor()))
}
}
pub fn aplicar_permissoes_600(caminho: &std::path::Path) -> Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(caminho)
.with_context(|| format!("Falha ao ler metadados de: {}", caminho.display()))?
.permissions();
perms.set_mode(0o600);
std::fs::set_permissions(caminho, perms)
.with_context(|| format!("Falha ao definir permissões em: {}", caminho.display()))?;
}
#[cfg(not(unix))]
let _ = caminho;
Ok(())
}
fn resolver_home_override() -> Option<PathBuf> {
let home = std::env::var("CONTEXT7_HOME").ok()?;
if home.is_empty() {
return None;
}
let base = PathBuf::from(&home);
if base
.components()
.any(|c| c == std::path::Component::ParentDir)
{
tracing::warn!(
"CONTEXT7_HOME='{}' rejeitado (path traversal) — usando padrão XDG",
home
);
return None;
}
let nomes_reservados = [
"CON", "PRN", "AUX", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7",
"COM8", "COM9", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8",
"LPT9",
];
for componente in base.components() {
if let std::path::Component::Normal(nome) = componente {
let nome_upper = nome.to_string_lossy().to_uppercase();
let nome_base = nome_upper.split('.').next().unwrap_or("");
if nomes_reservados.contains(&nome_base) {
tracing::warn!(
"CONTEXT7_HOME='{}' rejeitado (nome reservado Windows '{}') — usando padrão XDG",
home, nome_base
);
return None;
}
}
}
Some(base)
}
pub fn descobrir_caminho_config() -> Option<PathBuf> {
let caminho = if let Some(base) = resolver_home_override() {
base.join("context7").join("config.toml")
} else {
ProjectDirs::from("", "", "context7")?
.config_dir()
.join("config.toml")
};
let caminho_str = caminho.to_string_lossy().nfc().collect::<String>();
Some(PathBuf::from(caminho_str))
}
pub fn descobrir_caminho_logs_xdg() -> Option<PathBuf> {
let caminho = if let Some(base) = resolver_home_override() {
base.join("context7").join("logs")
} else {
let dirs = ProjectDirs::from("", "", "context7")?;
#[cfg(target_os = "linux")]
{
dirs.state_dir()
.unwrap_or_else(|| dirs.data_local_dir())
.to_path_buf()
}
#[cfg(not(target_os = "linux"))]
{
dirs.data_local_dir().to_path_buf()
}
};
let caminho_str = caminho.to_string_lossy().nfc().collect::<String>();
Some(PathBuf::from(caminho_str))
}
pub fn ler_env_var_chave() -> Option<Vec<String>> {
std::env::var("CONTEXT7_API_KEYS")
.ok()
.map(|valor| {
let estimativa = valor.matches(',').count() + 1;
let mut chaves = Vec::with_capacity(estimativa);
for s in valor.split(',') {
let trimmed = s.trim().to_string();
if !trimmed.is_empty() {
chaves.push(trimmed);
}
}
chaves
})
.filter(|v| !v.is_empty())
}
pub fn ler_config_xdg() -> Result<Option<Vec<String>>> {
let caminho = match descobrir_caminho_config() {
Some(p) => p,
None => return Ok(None),
};
if !caminho.exists() {
return Ok(None);
}
let conteudo = std::fs::read_to_string(&caminho)
.with_context(|| format!("Falha ao ler configuração XDG em: {}", caminho.display()))?;
let config: ConfigArquivo = toml::from_str(&conteudo)
.with_context(|| format!("TOML inválido em: {}", caminho.display()))?;
let chaves: Vec<String> = config
.keys
.into_iter()
.map(|c| c.value)
.filter(|v| !v.is_empty())
.collect();
if chaves.is_empty() {
Ok(None)
} else {
Ok(Some(chaves))
}
}
pub fn ler_env_cwd() -> Option<Vec<String>> {
let caminho = std::env::current_dir().ok().map(|d| d.join(".env"))?;
if !caminho.exists() {
return None;
}
std::fs::read_to_string(&caminho)
.ok()
.and_then(|conteudo| extrair_chaves_env(&conteudo).ok())
}
pub fn ler_env_compile_time() -> Option<Vec<String>> {
option_env!("CONTEXT7_API_KEYS").map(|valor| {
let estimativa = valor.matches(',').count() + 1;
let mut chaves = Vec::with_capacity(estimativa);
for s in valor.split(',') {
let trimmed = s.trim().to_string();
if !trimmed.is_empty() {
chaves.push(trimmed);
}
}
chaves
})
}
pub fn carregar_chaves_api() -> Result<Vec<ChaveApi>> {
use tracing::{info, warn};
if let Some(chaves) = ler_env_var_chave() {
info!("Chaves carregadas via variável de ambiente CONTEXT7_API_KEYS");
return Ok(chaves.into_iter().map(ChaveApi::new).collect());
}
match ler_config_xdg() {
Ok(Some(chaves)) => {
info!("Chaves carregadas via configuração XDG");
return Ok(chaves.into_iter().map(ChaveApi::new).collect());
}
Ok(None) => {}
Err(e) => {
warn!("Falha ao ler configuração XDG (continuando): {}", e);
}
}
if let Some(chaves) = ler_env_cwd() {
info!(
"Iniciando context7 com {} chaves de API disponíveis",
chaves.len()
);
return Ok(chaves.into_iter().map(ChaveApi::new).collect());
}
if let Some(chaves) = ler_env_compile_time() {
info!("Chaves carregadas via compile-time CONTEXT7_API_KEYS");
return Ok(chaves.into_iter().map(ChaveApi::new).collect());
}
bail!(t(Mensagem::NenhumaChaveConfigurada))
}
pub fn escrever_config_xdg(nova_chave: &str) -> Result<PathBuf> {
let caminho = descobrir_caminho_config()
.context("Sistema não suporta diretórios XDG — impossível salvar configuração")?;
if let Some(pai) = caminho.parent() {
std::fs::create_dir_all(pai)
.with_context(|| format!("Falha ao criar diretório: {}", pai.display()))?;
}
let mut config = if caminho.exists() {
let conteudo = std::fs::read_to_string(&caminho)
.with_context(|| format!("Falha ao ler config existente: {}", caminho.display()))?;
toml::from_str::<ConfigArquivo>(&conteudo)
.with_context(|| format!("TOML inválido em: {}", caminho.display()))?
} else {
ConfigArquivo {
schema_version: 1,
keys: Vec::new(),
}
};
let ja_existe = config.keys.iter().any(|c| c.value == nova_chave);
if !ja_existe {
config.keys.push(ChaveArmazenada {
value: nova_chave.to_string(),
added_at: Utc::now().to_rfc3339(),
});
}
let toml_str =
toml::to_string_pretty(&config).context("Falha ao serializar configuração para TOML")?;
std::fs::write(&caminho, &toml_str)
.with_context(|| format!("Falha ao escrever config em: {}", caminho.display()))?;
aplicar_permissoes_600(&caminho)?;
Ok(caminho)
}
pub fn ler_config_xdg_raw() -> Result<Option<ConfigArquivo>> {
let caminho = match descobrir_caminho_config() {
Some(p) => p,
None => return Ok(None),
};
if !caminho.exists() {
return Ok(None);
}
let conteudo = std::fs::read_to_string(&caminho)
.with_context(|| format!("Falha ao ler configuração XDG em: {}", caminho.display()))?;
let config: ConfigArquivo = toml::from_str(&conteudo)
.with_context(|| format!("TOML inválido em: {}", caminho.display()))?;
Ok(Some(config))
}
pub fn escrever_config_arquivo(config: &ConfigArquivo) -> Result<PathBuf> {
let caminho = descobrir_caminho_config()
.context("Sistema não suporta diretórios XDG — impossível salvar configuração")?;
if let Some(pai) = caminho.parent() {
std::fs::create_dir_all(pai)
.with_context(|| format!("Falha ao criar diretório: {}", pai.display()))?;
}
let toml_str =
toml::to_string_pretty(config).context("Falha ao serializar configuração para TOML")?;
std::fs::write(&caminho, &toml_str)
.with_context(|| format!("Falha ao escrever config em: {}", caminho.display()))?;
aplicar_permissoes_600(&caminho)?;
Ok(caminho)
}
pub fn mascarar_chave(chave: &str) -> String {
let n_chars = chave.chars().count();
let inicio = 12;
let fim = 4;
if n_chars <= inicio + fim {
return "***".to_string();
}
let prefixo: String = chave.chars().take(inicio).collect();
let sufixo: String = chave
.chars()
.rev()
.take(fim)
.collect::<String>()
.chars()
.rev()
.collect();
format!("{}...{}", prefixo, sufixo)
}
pub fn extrair_chaves_env(conteudo: &str) -> Result<Vec<String>> {
let chaves: Vec<String> = conteudo
.lines()
.filter_map(|linha| {
let linha_sem_comentario = linha.split('#').next().unwrap_or("").trim();
linha_sem_comentario
.strip_prefix("CONTEXT7_API=")
.map(|valor| {
valor
.trim()
.trim_matches('"')
.trim_matches('\'')
.to_string()
})
.filter(|v| !v.is_empty())
})
.collect();
if chaves.is_empty() {
bail!(t(Mensagem::NenhumaChaveContext7NoArquivo));
}
Ok(chaves)
}
pub fn cmd_keys_add(chave: &str) -> Result<()> {
let chave_trimmed = chave.trim();
if chave_trimmed.is_empty() {
crate::output::exibir_chave_invalida_vazia();
bail!(ErroContext7::OperacaoKeysFalhou);
}
if !chave_trimmed.starts_with("ctx7sk-") || chave_trimmed.len() < 16 {
crate::output::exibir_aviso_formato_chave();
}
if let Some(config) = ler_config_xdg_raw()? {
if config.keys.iter().any(|c| c.value == chave_trimmed) {
crate::output::exibir_chave_ja_existia();
return Ok(());
}
}
let caminho = escrever_config_xdg(chave_trimmed)?;
crate::output::exibir_chave_adicionada(&caminho);
Ok(())
}
pub fn cmd_keys_list(json: bool) -> Result<()> {
match ler_config_xdg_raw()? {
None => {
if json {
crate::output::exibir_json_array_vazio();
} else {
crate::output::exibir_nenhuma_chave();
}
}
Some(config) if config.keys.is_empty() => {
if json {
crate::output::exibir_json_array_vazio();
} else {
crate::output::exibir_nenhuma_chave();
}
}
Some(config) => {
if json {
let mut mascaradas: Vec<serde_json::Value> = Vec::with_capacity(config.keys.len());
mascaradas.extend(config.keys.iter().enumerate().map(|(i, k)| {
serde_json::json!({
"index": i + 1,
"masked_key": mascarar_chave(&k.value),
"added_at": crate::output::formatar_added_at_display(&k.added_at)
})
}));
crate::output::exibir_json_bruto(
&serde_json::to_string_pretty(&mascaradas).with_context(|| {
crate::i18n::t(crate::i18n::Mensagem::FalhaSerializarJson)
})?,
);
} else {
crate::output::exibir_chaves_mascaradas(&config.keys, mascarar_chave);
}
}
}
Ok(())
}
pub fn cmd_keys_remove(indice: usize) -> Result<()> {
let mut config = match ler_config_xdg_raw()? {
None => {
crate::output::exibir_nenhuma_chave_para_remover();
bail!(ErroContext7::OperacaoKeysFalhou);
}
Some(c) if c.keys.is_empty() => {
crate::output::exibir_nenhuma_chave_para_remover();
bail!(ErroContext7::OperacaoKeysFalhou);
}
Some(c) => c,
};
if indice == 0 || indice > config.keys.len() {
crate::output::exibir_indice_invalido(indice, config.keys.len());
bail!(ErroContext7::OperacaoKeysFalhou);
}
let removida = config.keys.remove(indice - 1);
escrever_config_arquivo(&config)?;
crate::output::exibir_chave_removida(&mascarar_chave(&removida.value));
Ok(())
}
pub fn cmd_keys_clear(sim: bool) -> Result<()> {
if !sim && !crate::output::confirmar_clear()? {
crate::output::exibir_operacao_cancelada();
return Ok(());
}
let config = ConfigArquivo {
schema_version: 1,
keys: Vec::new(),
};
escrever_config_arquivo(&config)?;
crate::output::exibir_chaves_removidas();
Ok(())
}
pub fn cmd_keys_path() -> Result<()> {
match descobrir_caminho_config() {
Some(caminho) => crate::output::exibir_caminho_config(&caminho),
None => crate::output::exibir_xdg_nao_suportado(),
}
Ok(())
}
pub fn cmd_keys_import(arquivo: &std::path::Path) -> Result<()> {
let conteudo = std::fs::read_to_string(arquivo)
.with_context(|| format!("Falha ao ler arquivo: {}", arquivo.display()))?;
let chaves =
extrair_chaves_env(&conteudo).with_context(|| format!("Arquivo: {}", arquivo.display()))?;
let total = chaves.len();
let mut importadas = 0usize;
for chave in &chaves {
escrever_config_xdg(chave)?;
importadas += 1;
}
crate::output::exibir_importacao_concluida(importadas, total);
Ok(())
}
pub fn cmd_keys_export() -> Result<()> {
match ler_config_xdg_raw()? {
None => {}
Some(config) if config.keys.is_empty() => {}
Some(config) => {
for chave in &config.keys {
crate::output::exibir_chave_exportada(&chave.value);
}
}
}
Ok(())
}
#[cfg(test)]
mod testes {
use super::*;
fn ler_config_toml_do_caminho(caminho: &std::path::Path) -> Result<ConfigArquivo> {
let conteudo = std::fs::read_to_string(caminho)
.with_context(|| format!("Falha ao ler: {}", caminho.display()))?;
toml::from_str(&conteudo)
.with_context(|| format!("TOML inválido em: {}", caminho.display()))
}
#[test]
fn testa_parsing_env_com_multiplas_chaves_iguais() {
let mut conteudo = String::new();
for i in 0..17 {
conteudo.push_str(&format!("CONTEXT7_API=ctx7sk-chave-{:02}\n", i));
}
let chaves = extrair_chaves_env(&conteudo).expect("Deve extrair 17 chaves sem erro");
assert_eq!(chaves.len(), 17, "Deve retornar exatamente 17 chaves");
for (i, chave) in chaves.iter().enumerate() {
assert_eq!(
chave,
&format!("ctx7sk-chave-{:02}", i),
"Chave {} deve ter o valor correto",
i
);
}
}
#[test]
fn testa_parsing_env_ignora_comentarios_e_linhas_vazias() {
let conteudo = "# Este é um comentário\n\
CONTEXT7_API=ctx7sk-chave-valida-01\n\
\n\
# Outro comentário\n\
CONTEXT7_API=ctx7sk-chave-valida-02\n\
\n";
let chaves = extrair_chaves_env(conteudo).expect("Deve extrair chaves sem erro");
assert_eq!(chaves.len(), 2, "Deve ignorar comentários e linhas vazias");
assert_eq!(chaves[0], "ctx7sk-chave-valida-01");
assert_eq!(chaves[1], "ctx7sk-chave-valida-02");
}
#[test]
fn testa_parsing_env_remove_aspas_duplas() {
let conteudo = "CONTEXT7_API=\"ctx7sk-abc-com-aspas\"\n";
let chaves = extrair_chaves_env(conteudo).expect("Deve extrair chave sem erro");
assert_eq!(chaves.len(), 1);
assert_eq!(
chaves[0], "ctx7sk-abc-com-aspas",
"Deve remover aspas duplas"
);
}
#[test]
fn testa_parsing_env_remove_aspas_simples() {
let conteudo = "CONTEXT7_API='ctx7sk-abc-aspas-simples'\n";
let chaves = extrair_chaves_env(conteudo).expect("Deve extrair chave sem erro");
assert_eq!(chaves.len(), 1);
assert_eq!(
chaves[0], "ctx7sk-abc-aspas-simples",
"Deve remover aspas simples"
);
}
#[test]
fn testa_parsing_env_erro_quando_nenhuma_chave() {
let conteudo = "# Apenas comentários\n\
OUTRA_VAR=valor\n\
\n";
let resultado = extrair_chaves_env(conteudo);
assert!(
resultado.is_err(),
"Deve retornar Err quando não há chaves CONTEXT7_API"
);
let mensagem_erro = resultado.unwrap_err().to_string();
assert!(
mensagem_erro.contains("chave")
|| mensagem_erro.contains("CONTEXT7_API")
|| mensagem_erro.contains("key")
|| mensagem_erro.contains("API"),
"Mensagem de erro deve mencionar CONTEXT7_API, chave, key ou API, obteve: {}",
mensagem_erro
);
}
#[test]
fn testa_parsing_env_ignora_chaves_vazias() {
let conteudo = "CONTEXT7_API=\n\
CONTEXT7_API=ctx7sk-valida\n";
let chaves = extrair_chaves_env(conteudo).expect("Deve extrair chave sem erro");
assert_eq!(
chaves.len(),
1,
"Deve ignorar entradas CONTEXT7_API sem valor"
);
assert_eq!(chaves[0], "ctx7sk-valida");
}
#[test]
fn testa_parsing_env_ignora_comentario_inline() {
let conteudo = "CONTEXT7_API=ctx7sk-valida # comentário aqui\n";
let chaves = extrair_chaves_env(conteudo).expect("Deve extrair chave sem erro");
assert_eq!(chaves.len(), 1);
assert_eq!(chaves[0], "ctx7sk-valida");
}
#[test]
fn testa_parsing_env_com_line_endings_crlf() {
let conteudo = "CONTEXT7_API=ctx7sk-crlf-chave-a\r\nCONTEXT7_API=ctx7sk-crlf-chave-b\r\n";
let chaves =
extrair_chaves_env(conteudo).expect("Deve extrair 2 chaves de conteúdo CRLF sem erro");
assert_eq!(
chaves.len(),
2,
"Deve retornar exatamente 2 chaves com CRLF"
);
assert_eq!(
chaves[0], "ctx7sk-crlf-chave-a",
"Primeira chave não deve conter \\r residual"
);
assert_eq!(
chaves[1], "ctx7sk-crlf-chave-b",
"Segunda chave não deve conter \\r residual"
);
}
#[test]
fn testa_parsing_env_com_line_endings_mixed() {
let conteudo = "CONTEXT7_API=ctx7sk-mixed-chave-a\nCONTEXT7_API=ctx7sk-mixed-chave-b\r\n";
let chaves = extrair_chaves_env(conteudo)
.expect("Deve extrair 2 chaves de conteúdo misto LF/CRLF sem erro");
assert_eq!(
chaves.len(),
2,
"Deve retornar exatamente 2 chaves com line endings mistos"
);
assert_eq!(
chaves[0], "ctx7sk-mixed-chave-a",
"Chave com LF não deve ter \\r residual"
);
assert_eq!(
chaves[1], "ctx7sk-mixed-chave-b",
"Chave com CRLF não deve ter \\r residual"
);
}
#[test]
fn testa_mascarar_chave_com_valor_longo_exibe_prefixo_e_sufixo() {
let chave = "ctx7sk-abc123-def456-ghi789";
assert_eq!(chave.len(), 27, "Pré-condição: chave deve ter 27 chars");
let mascarada = mascarar_chave(chave);
assert!(
mascarada.starts_with("ctx7sk-abc12"),
"Deve iniciar com os primeiros 12 chars, obteve: {}",
mascarada
);
assert!(
mascarada.ends_with("i789"),
"Deve terminar com os últimos 4 chars, obteve: {}",
mascarada
);
assert!(
mascarada.contains("..."),
"Deve conter '...' entre prefixo e sufixo, obteve: {}",
mascarada
);
}
#[test]
fn testa_mascarar_chave_curta_retorna_asteriscos() {
let chave_exatamente_16 = "ctx7sk-abcdef012";
assert_eq!(
chave_exatamente_16.len(),
16,
"Pré-condição: chave deve ter 16 chars"
);
let mascarada = mascarar_chave(chave_exatamente_16);
assert_eq!(
mascarada, "***",
"Chave de 16 chars deve retornar '***', obteve: {}",
mascarada
);
}
#[test]
fn testa_mascarar_chave_vazia_retorna_asteriscos() {
let mascarada = mascarar_chave("");
assert_eq!(
mascarada, "***",
"Chave vazia deve retornar '***', obteve: {}",
mascarada
);
}
#[test]
fn testa_mascarar_chave_de_exatamente_17_chars_mascara_corretamente() {
let chave = "ctx7sk-abcdef0123"; assert_eq!(chave.len(), 17, "Pré-condição: chave deve ter 17 chars");
let mascarada = mascarar_chave(chave);
assert!(
mascarada.contains("..."),
"Chave de 17 chars deve ser mascarada, obteve: {}",
mascarada
);
assert_eq!(
&mascarada[..12],
&chave[..12],
"Prefixo de 12 chars deve ser preservado"
);
assert!(
mascarada.ends_with(&chave[chave.len() - 4..]),
"Sufixo de 4 chars deve ser preservado"
);
}
#[test]
#[serial_test::serial]
fn testa_ler_env_var_chave_retorna_some_quando_setada() {
unsafe {
std::env::set_var("CONTEXT7_API_KEYS", "ctx7sk-chave-teste-01");
}
let resultado = ler_env_var_chave();
unsafe {
std::env::remove_var("CONTEXT7_API_KEYS");
}
let chaves = resultado.expect("Deve retornar Some com chave válida");
assert_eq!(chaves.len(), 1, "Deve retornar exatamente 1 chave");
assert_eq!(chaves[0], "ctx7sk-chave-teste-01");
}
#[test]
#[serial_test::serial]
fn testa_ler_env_var_chave_aceita_multiplas_separadas_por_virgula() {
unsafe {
std::env::set_var(
"CONTEXT7_API_KEYS",
"ctx7sk-chave-a, ctx7sk-chave-b , ctx7sk-chave-c",
);
}
let resultado = ler_env_var_chave();
unsafe {
std::env::remove_var("CONTEXT7_API_KEYS");
}
let chaves = resultado.expect("Deve retornar Some com múltiplas chaves");
assert_eq!(chaves.len(), 3, "Deve retornar 3 chaves");
assert_eq!(chaves[0], "ctx7sk-chave-a");
assert_eq!(chaves[1], "ctx7sk-chave-b");
assert_eq!(chaves[2], "ctx7sk-chave-c");
}
#[test]
#[serial_test::serial]
fn testa_ler_env_var_chave_retorna_none_quando_vazia() {
unsafe {
std::env::set_var("CONTEXT7_API_KEYS", "");
}
let resultado = ler_env_var_chave();
unsafe {
std::env::remove_var("CONTEXT7_API_KEYS");
}
assert!(
resultado.is_none(),
"Deve retornar None quando env var está vazia"
);
}
#[test]
#[serial_test::serial]
fn testa_ler_env_var_chave_retorna_none_quando_apenas_whitespace() {
unsafe {
std::env::set_var("CONTEXT7_API_KEYS", " , , ");
}
let resultado = ler_env_var_chave();
unsafe {
std::env::remove_var("CONTEXT7_API_KEYS");
}
assert!(
resultado.is_none(),
"Deve retornar None quando env var contém apenas whitespace/vírgulas"
);
}
#[test]
#[serial_test::serial]
fn testa_ler_env_var_chave_retorna_none_quando_ausente() {
unsafe {
std::env::remove_var("CONTEXT7_API_KEYS");
}
let resultado = ler_env_var_chave();
assert!(
resultado.is_none(),
"Deve retornar None quando env var não existe"
);
}
#[test]
#[serial_test::serial]
fn testa_context7_home_rejeita_path_traversal() {
let casos = ["../../../etc", "..", "/tmp/../etc"];
for caso in &casos {
unsafe {
std::env::set_var("CONTEXT7_HOME", caso);
}
let resultado = descobrir_caminho_config();
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
if let Some(caminho) = resultado {
let s = caminho.to_string_lossy();
assert!(
!s.contains(".."),
"Path traversal '{caso}' não deve resultar em caminho com '..': {s}"
);
}
}
}
#[test]
#[serial_test::serial]
fn testa_ler_config_xdg_arquivo_inexistente_retorna_none() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
unsafe {
std::env::set_var("CONTEXT7_HOME", dir_temp.path());
}
let resultado = ler_config_xdg();
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
let valor = resultado.expect("Deve retornar Ok quando arquivo não existe");
assert!(
valor.is_none(),
"Deve retornar None quando config.toml não existe"
);
}
#[test]
#[serial_test::serial]
fn testa_ler_config_xdg_le_toml_valido_com_multiplas_chaves() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
let dir_context7 = dir_temp.path().join("context7");
std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
let toml_conteudo = r#"schema_version = 1
[[keys]]
value = "ctx7sk-chave-xdg-01"
added_at = "2026-01-01T00:00:00+00:00"
[[keys]]
value = "ctx7sk-chave-xdg-02"
added_at = "2026-01-02T00:00:00+00:00"
"#;
std::fs::write(dir_context7.join("config.toml"), toml_conteudo)
.expect("Deve escrever config.toml");
unsafe {
std::env::set_var("CONTEXT7_HOME", dir_temp.path());
}
let resultado = ler_config_xdg();
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
let chaves = resultado
.expect("Deve retornar Ok")
.expect("Deve retornar Some com chaves");
assert_eq!(chaves.len(), 2, "Deve retornar 2 chaves");
assert_eq!(chaves[0], "ctx7sk-chave-xdg-01");
assert_eq!(chaves[1], "ctx7sk-chave-xdg-02");
}
#[test]
#[serial_test::serial]
fn testa_ler_config_xdg_retorna_err_quando_toml_invalido() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
let dir_context7 = dir_temp.path().join("context7");
std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
std::fs::write(
dir_context7.join("config.toml"),
"schema_version = INVALIDO\n[[[malformado",
)
.expect("Deve escrever TOML inválido");
unsafe {
std::env::set_var("CONTEXT7_HOME", dir_temp.path());
}
let resultado = ler_config_xdg();
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
assert!(
resultado.is_err(),
"Deve retornar Err quando TOML está malformado"
);
}
#[test]
#[serial_test::serial]
fn testa_ler_config_xdg_preserva_ordem_das_chaves() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
let dir_context7 = dir_temp.path().join("context7");
std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
let toml_conteudo = r#"schema_version = 1
[[keys]]
value = "ctx7sk-primeira"
added_at = "2026-01-01T00:00:00+00:00"
[[keys]]
value = "ctx7sk-segunda"
added_at = "2026-01-02T00:00:00+00:00"
[[keys]]
value = "ctx7sk-terceira"
added_at = "2026-01-03T00:00:00+00:00"
"#;
std::fs::write(dir_context7.join("config.toml"), toml_conteudo)
.expect("Deve escrever config.toml");
unsafe {
std::env::set_var("CONTEXT7_HOME", dir_temp.path());
}
let resultado = ler_config_xdg();
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
let chaves = resultado
.expect("Deve retornar Ok")
.expect("Deve retornar Some");
assert_eq!(chaves[0], "ctx7sk-primeira");
assert_eq!(chaves[1], "ctx7sk-segunda");
assert_eq!(chaves[2], "ctx7sk-terceira");
}
#[test]
#[serial_test::serial]
fn testa_ler_config_xdg_keys_vazio_retorna_none() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
let dir_context7 = dir_temp.path().join("context7");
std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
let toml_sem_chaves = "schema_version = 1\n";
std::fs::write(dir_context7.join("config.toml"), toml_sem_chaves)
.expect("Deve escrever config.toml sem keys");
unsafe {
std::env::set_var("CONTEXT7_HOME", dir_temp.path());
}
let resultado = ler_config_xdg();
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
let valor = resultado.expect("Deve retornar Ok");
assert!(
valor.is_none(),
"Deve retornar None quando config.toml existe mas keys está vazio"
);
}
#[test]
#[serial_test::serial]
fn testa_escrever_config_xdg_roundtrip_serializa_e_deserializa() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
unsafe {
std::env::set_var("CONTEXT7_HOME", dir_temp.path());
}
let caminho =
escrever_config_xdg("ctx7sk-roundtrip-01").expect("Deve escrever config sem erro");
let config_lido = ler_config_toml_do_caminho(&caminho)
.expect("Deve ler TOML escrito por escrever_config_xdg");
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
assert_eq!(config_lido.schema_version, 1, "schema_version deve ser 1");
assert_eq!(config_lido.keys.len(), 1, "Deve conter 1 chave");
assert_eq!(
config_lido.keys[0].value, "ctx7sk-roundtrip-01",
"Valor da chave deve ser preservado"
);
assert!(
!config_lido.keys[0].added_at.is_empty(),
"added_at não deve ser vazio"
);
}
#[test]
#[serial_test::serial]
fn testa_escrever_config_xdg_cria_diretorios_pai_se_nao_existirem() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
let xdg_novo = dir_temp.path().join("xdg_inexistente");
unsafe {
std::env::set_var("CONTEXT7_HOME", &xdg_novo);
}
let resultado = escrever_config_xdg("ctx7sk-mkdir-teste");
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
let caminho = resultado.expect("Deve criar diretório pai e escrever config");
assert!(
caminho.exists(),
"Arquivo de config deve existir após escrita"
);
}
#[test]
#[serial_test::serial]
fn testa_escrever_config_xdg_nao_duplica_chave_ja_existente() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
unsafe {
std::env::set_var("CONTEXT7_HOME", dir_temp.path());
}
escrever_config_xdg("ctx7sk-unica").expect("Primeira escrita deve funcionar");
escrever_config_xdg("ctx7sk-unica").expect("Segunda escrita não deve falhar");
let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config");
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
assert_eq!(
config.keys.len(),
1,
"Não deve duplicar chave já existente — deve ter apenas 1"
);
}
#[test]
#[serial_test::serial]
fn testa_escrever_config_xdg_acumula_chaves_distintas() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
unsafe {
std::env::set_var("CONTEXT7_HOME", dir_temp.path());
}
escrever_config_xdg("ctx7sk-chave-a").expect("Primeira escrita deve funcionar");
escrever_config_xdg("ctx7sk-chave-b").expect("Segunda escrita deve funcionar");
escrever_config_xdg("ctx7sk-chave-c").expect("Terceira escrita deve funcionar");
let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config");
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
assert_eq!(config.keys.len(), 3, "Deve acumular 3 chaves distintas");
let valores: Vec<&str> = config.keys.iter().map(|c| c.value.as_str()).collect();
assert!(valores.contains(&"ctx7sk-chave-a"));
assert!(valores.contains(&"ctx7sk-chave-b"));
assert!(valores.contains(&"ctx7sk-chave-c"));
}
#[test]
#[cfg(unix)]
#[serial_test::serial]
fn testa_escrever_config_xdg_aplica_permissoes_600_em_unix() {
use std::os::unix::fs::PermissionsExt;
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
unsafe {
std::env::set_var("CONTEXT7_HOME", dir_temp.path());
}
let caminho =
escrever_config_xdg("ctx7sk-perm-600").expect("Deve escrever config sem erro");
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
let metadados = std::fs::metadata(&caminho).expect("Deve obter metadados do arquivo");
let modo = metadados.permissions().mode() & 0o777;
assert_eq!(modo, 0o600, "Permissões devem ser 600, obteve: {:o}", modo);
}
#[test]
fn testa_config_arquivo_roundtrip_serde_preserva_todos_campos() {
let config_original = ConfigArquivo {
schema_version: 1,
keys: vec![
ChaveArmazenada {
value: "ctx7sk-serde-01".to_string(),
added_at: "2026-01-01T12:00:00+00:00".to_string(),
},
ChaveArmazenada {
value: "ctx7sk-serde-02".to_string(),
added_at: "2026-01-02T12:00:00+00:00".to_string(),
},
],
};
let toml_str = toml::to_string_pretty(&config_original)
.expect("Deve serializar ConfigArquivo para TOML");
let config_deserializado: ConfigArquivo =
toml::from_str(&toml_str).expect("Deve deserializar TOML de volta para ConfigArquivo");
assert_eq!(
config_deserializado.schema_version, config_original.schema_version,
"schema_version deve ser preservado no roundtrip"
);
assert_eq!(
config_deserializado.keys.len(),
config_original.keys.len(),
"Número de chaves deve ser preservado"
);
assert_eq!(
config_deserializado.keys[0].value, config_original.keys[0].value,
"Valor da primeira chave deve ser preservado"
);
assert_eq!(
config_deserializado.keys[0].added_at, config_original.keys[0].added_at,
"added_at da primeira chave deve ser preservado"
);
}
#[test]
fn testa_config_arquivo_schema_version_sempre_presente_na_serializacao() {
let config = ConfigArquivo {
schema_version: 1,
keys: Vec::new(),
};
let toml_str = toml::to_string_pretty(&config).expect("Deve serializar para TOML");
assert!(
toml_str.contains("schema_version"),
"schema_version deve estar presente na serialização TOML"
);
assert!(toml_str.contains('1'), "Valor 1 deve estar presente");
}
#[test]
fn testa_config_arquivo_keys_vazio_aceito_na_deserializacao() {
let toml_str = "schema_version = 1\n";
let config: ConfigArquivo =
toml::from_str(toml_str).expect("Deve deserializar com keys ausente (default vazio)");
assert_eq!(config.schema_version, 1);
assert!(
config.keys.is_empty(),
"keys deve ser vazio quando não presente no TOML"
);
}
#[test]
fn testa_chave_armazenada_preserva_added_at_como_string_utc() {
let timestamp = "2026-04-08T20:00:00+00:00";
let chave = ChaveArmazenada {
value: "ctx7sk-timestamp".to_string(),
added_at: timestamp.to_string(),
};
let toml_str = toml::to_string_pretty(&chave).expect("Deve serializar ChaveArmazenada");
let chave_de_volta: ChaveArmazenada =
toml::from_str(&toml_str).expect("Deve deserializar ChaveArmazenada");
assert_eq!(
chave_de_volta.added_at, timestamp,
"Timestamp added_at deve ser preservado exatamente"
);
}
#[test]
#[serial_test::serial]
fn testa_carregar_chaves_api_env_var_tem_prioridade_sobre_xdg() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
let dir_context7 = dir_temp.path().join("context7");
std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
let toml_xdg = r#"schema_version = 1
[[keys]]
value = "ctx7sk-xdg-deve-ser-ignorada"
added_at = "2026-01-01T00:00:00+00:00"
"#;
std::fs::write(dir_context7.join("config.toml"), toml_xdg)
.expect("Deve escrever config XDG");
unsafe {
std::env::set_var("CONTEXT7_API_KEYS", "ctx7sk-env-var-prioritaria");
std::env::set_var("CONTEXT7_HOME", dir_temp.path());
}
let resultado = carregar_chaves_api();
unsafe {
std::env::remove_var("CONTEXT7_API_KEYS");
std::env::remove_var("CONTEXT7_HOME");
}
let chaves = resultado.expect("Deve carregar chaves via env var");
assert_eq!(chaves.len(), 1);
assert_eq!(
chaves[0], "ctx7sk-env-var-prioritaria",
"Env var deve ter prioridade sobre XDG"
);
}
#[test]
#[serial_test::serial]
fn testa_carregar_chaves_api_xdg_usado_quando_env_var_ausente() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
let dir_context7 = dir_temp.path().join("context7");
std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
let toml_xdg = r#"schema_version = 1
[[keys]]
value = "ctx7sk-via-xdg"
added_at = "2026-01-01T00:00:00+00:00"
"#;
std::fs::write(dir_context7.join("config.toml"), toml_xdg)
.expect("Deve escrever config XDG");
unsafe {
std::env::remove_var("CONTEXT7_API_KEYS");
std::env::set_var("CONTEXT7_HOME", dir_temp.path());
}
let resultado = carregar_chaves_api();
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
let chaves = resultado.expect("Deve carregar chaves via XDG");
assert_eq!(chaves.len(), 1);
assert_eq!(chaves[0], "ctx7sk-via-xdg");
}
#[test]
#[serial_test::serial]
fn testa_carregar_chaves_api_retorna_err_quando_nada_disponivel() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
let dir_xdg_vazio = dir_temp.path().join("xdg_vazio");
std::fs::create_dir_all(&dir_xdg_vazio).expect("Deve criar diretório XDG vazio");
let dir_sem_env = dir_temp.path().join("sem_env");
std::fs::create_dir_all(&dir_sem_env).expect("Deve criar diretório sem .env");
unsafe {
std::env::remove_var("CONTEXT7_API_KEYS");
std::env::set_var("CONTEXT7_HOME", &dir_xdg_vazio);
}
let cwd_original = std::env::current_dir().expect("Deve obter CWD atual");
std::env::set_current_dir(&dir_sem_env).expect("Deve mudar CWD");
let resultado = carregar_chaves_api();
std::env::set_current_dir(&cwd_original).expect("Deve restaurar CWD");
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
assert!(
resultado.is_err(),
"Deve retornar Err quando nenhuma camada fornecer chaves"
);
}
#[test]
#[serial_test::serial]
fn testa_ler_env_cwd_le_env_com_multiplas_chaves_context7_api() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
let conteudo_env = "CONTEXT7_API=ctx7sk-cwd-01\nCONTEXT7_API=ctx7sk-cwd-02\n";
std::fs::write(dir_temp.path().join(".env"), conteudo_env)
.expect("Deve escrever .env temporário");
let cwd_original = std::env::current_dir().expect("Deve obter CWD");
std::env::set_current_dir(dir_temp.path()).expect("Deve mudar CWD para temp");
let resultado = ler_env_cwd();
std::env::set_current_dir(&cwd_original).expect("Deve restaurar CWD");
let chaves = resultado.expect("Deve retornar Some com chaves do .env CWD");
assert_eq!(chaves.len(), 2, "Deve ler 2 chaves do .env");
assert_eq!(chaves[0], "ctx7sk-cwd-01");
assert_eq!(chaves[1], "ctx7sk-cwd-02");
}
#[test]
#[serial_test::serial]
fn testa_ler_env_cwd_retorna_none_quando_env_ausente() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
let cwd_original = std::env::current_dir().expect("Deve obter CWD");
std::env::set_current_dir(dir_temp.path()).expect("Deve mudar CWD para temp sem .env");
let resultado = ler_env_cwd();
std::env::set_current_dir(&cwd_original).expect("Deve restaurar CWD");
assert!(
resultado.is_none(),
"Deve retornar None quando não há .env no CWD"
);
}
#[test]
fn testa_descobrir_caminho_logs_xdg_retorna_algum_caminho_valido() {
let resultado = descobrir_caminho_logs_xdg();
if let Some(caminho) = resultado {
let caminho_str = caminho.to_string_lossy();
assert!(
caminho_str.contains("context7"),
"Caminho de logs XDG deve conter 'context7', obteve: {}",
caminho_str
);
}
}
#[test]
#[serial_test::serial]
fn testa_carregar_chaves_api_env_cwd_usado_quando_env_var_e_xdg_ausentes() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
let dir_xdg_sem_config = dir_temp.path().join("xdg_sem_config");
std::fs::create_dir_all(&dir_xdg_sem_config).expect("Deve criar diretório XDG vazio");
let dir_cwd = dir_temp.path().join("cwd_com_env");
std::fs::create_dir_all(&dir_cwd).expect("Deve criar CWD temporário");
std::fs::write(dir_cwd.join(".env"), "CONTEXT7_API=ctx7sk-cwd-camada-3\n")
.expect("Deve escrever .env no CWD");
unsafe {
std::env::remove_var("CONTEXT7_API_KEYS");
std::env::set_var("CONTEXT7_HOME", &dir_xdg_sem_config);
}
let cwd_original = std::env::current_dir().expect("Deve obter CWD");
std::env::set_current_dir(&dir_cwd).expect("Deve mudar CWD");
let resultado = carregar_chaves_api();
std::env::set_current_dir(&cwd_original).expect("Deve restaurar CWD");
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
let chaves = resultado.expect("Deve carregar chaves via .env CWD");
assert_eq!(chaves.len(), 1);
assert_eq!(chaves[0], "ctx7sk-cwd-camada-3");
}
#[test]
#[serial_test::serial]
fn testa_carregar_chaves_api_faz_fallback_quando_xdg_invalido() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
let dir_context7 = dir_temp.path().join("context7");
std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
std::fs::write(dir_context7.join("config.toml"), "[[[invalido")
.expect("Deve escrever TOML inválido");
let dir_cwd = dir_temp.path().join("cwd_fallback");
std::fs::create_dir_all(&dir_cwd).expect("Deve criar CWD com .env");
std::fs::write(dir_cwd.join(".env"), "CONTEXT7_API=ctx7sk-fallback-cwd\n")
.expect("Deve escrever .env no CWD");
unsafe {
std::env::remove_var("CONTEXT7_API_KEYS");
std::env::set_var("CONTEXT7_HOME", dir_temp.path());
}
let cwd_original = std::env::current_dir().expect("Deve obter CWD");
std::env::set_current_dir(&dir_cwd).expect("Deve mudar CWD");
let resultado = carregar_chaves_api();
std::env::set_current_dir(&cwd_original).expect("Deve restaurar CWD");
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
let chaves = resultado.expect("Deve carregar chaves via fallback .env CWD");
assert_eq!(chaves.len(), 1);
assert_eq!(chaves[0], "ctx7sk-fallback-cwd");
}
#[test]
#[serial_test::serial]
fn testa_cmd_keys_add_cria_config_quando_nao_existe() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
unsafe {
std::env::set_var("CONTEXT7_HOME", dir_temp.path());
}
let resultado = cmd_keys_add("ctx7sk-nova-chave-add-test");
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
resultado.expect("cmd_keys_add deve funcionar em config vazio");
let caminho = dir_temp.path().join("context7").join("config.toml");
assert!(
caminho.exists(),
"config.toml deve existir após cmd_keys_add"
);
let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config criado");
assert_eq!(config.keys.len(), 1, "Config deve ter 1 chave");
assert_eq!(config.keys[0].value, "ctx7sk-nova-chave-add-test");
}
#[test]
#[serial_test::serial]
fn testa_cmd_keys_add_acumula_em_config_existente() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
unsafe {
std::env::set_var("CONTEXT7_HOME", dir_temp.path());
}
cmd_keys_add("ctx7sk-chave-um").expect("Primeira adição deve funcionar");
cmd_keys_add("ctx7sk-chave-dois").expect("Segunda adição deve funcionar");
let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config");
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
assert_eq!(config.keys.len(), 2, "Deve acumular 2 chaves");
assert_eq!(config.keys[0].value, "ctx7sk-chave-um");
assert_eq!(config.keys[1].value, "ctx7sk-chave-dois");
}
#[test]
#[serial_test::serial]
fn testa_cmd_keys_add_nao_duplica_chave_existente() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
unsafe {
std::env::set_var("CONTEXT7_HOME", dir_temp.path());
}
cmd_keys_add("ctx7sk-unica-dedup").expect("Primeira adição deve funcionar");
cmd_keys_add("ctx7sk-unica-dedup").expect("Segunda adição da mesma chave não deve falhar");
let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config");
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
assert_eq!(config.keys.len(), 1, "Não deve duplicar chave já existente");
}
#[test]
#[cfg(unix)]
#[serial_test::serial]
fn testa_cmd_keys_add_aplica_permissoes_600_em_unix() {
use std::os::unix::fs::PermissionsExt;
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
unsafe {
std::env::set_var("CONTEXT7_HOME", dir_temp.path());
}
cmd_keys_add("ctx7sk-perm-600-keys-add").expect("Deve adicionar chave sem erro");
let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
let metadados = std::fs::metadata(&caminho).expect("Deve obter metadados");
let modo = metadados.permissions().mode() & 0o777;
assert_eq!(
modo, 0o600,
"Permissões devem ser 600 após cmd_keys_add, obteve: {:o}",
modo
);
}
#[test]
#[serial_test::serial]
fn testa_cmd_keys_remove_indice_1_de_config_com_3_chaves() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
unsafe {
std::env::set_var("CONTEXT7_HOME", dir_temp.path());
}
escrever_config_xdg("ctx7sk-rem-alpha").expect("Deve escrever chave 1");
escrever_config_xdg("ctx7sk-rem-beta").expect("Deve escrever chave 2");
escrever_config_xdg("ctx7sk-rem-gamma").expect("Deve escrever chave 3");
cmd_keys_remove(1).expect("Remove índice 1 deve funcionar");
let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config");
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
assert_eq!(config.keys.len(), 2, "Devem sobrar 2 chaves após remoção");
assert_eq!(config.keys[0].value, "ctx7sk-rem-beta");
assert_eq!(config.keys[1].value, "ctx7sk-rem-gamma");
}
#[test]
#[serial_test::serial]
fn testa_cmd_keys_remove_indice_2_de_config_com_3_chaves() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
unsafe {
std::env::set_var("CONTEXT7_HOME", dir_temp.path());
}
escrever_config_xdg("ctx7sk-mid-alpha").expect("Deve escrever chave 1");
escrever_config_xdg("ctx7sk-mid-beta").expect("Deve escrever chave 2");
escrever_config_xdg("ctx7sk-mid-gamma").expect("Deve escrever chave 3");
cmd_keys_remove(2).expect("Remove índice 2 deve funcionar");
let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config");
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
assert_eq!(
config.keys.len(),
2,
"Devem sobrar 2 chaves após remoção da do meio"
);
assert_eq!(config.keys[0].value, "ctx7sk-mid-alpha");
assert_eq!(config.keys[1].value, "ctx7sk-mid-gamma");
}
#[test]
#[serial_test::serial]
fn testa_cmd_keys_remove_indice_zero_retorna_err_com_mensagem() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
unsafe {
std::env::set_var("CONTEXT7_HOME", dir_temp.path());
}
escrever_config_xdg("ctx7sk-idx-zero-test").expect("Deve escrever chave");
let resultado = cmd_keys_remove(0);
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
assert!(
resultado.is_err(),
"Índice 0 inválido deve retornar Err (exit code 1), obteve: {:?}",
resultado
);
}
#[test]
#[serial_test::serial]
fn testa_cmd_keys_remove_indice_maior_que_len_retorna_err_com_mensagem() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
unsafe {
std::env::set_var("CONTEXT7_HOME", dir_temp.path());
}
escrever_config_xdg("ctx7sk-overflow-test").expect("Deve escrever chave");
let resultado = cmd_keys_remove(99);
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
assert!(
resultado.is_err(),
"Índice fora do range deve retornar Err (exit code 1), obteve: {:?}",
resultado
);
}
#[test]
#[serial_test::serial]
fn testa_cmd_keys_remove_em_config_vazio_retorna_err() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
unsafe {
std::env::set_var("CONTEXT7_HOME", dir_temp.path());
}
let resultado = cmd_keys_remove(1);
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
assert!(
resultado.is_err(),
"Remover de config vazio deve retornar Err (exit code 1), obteve: {:?}",
resultado
);
}
#[test]
#[serial_test::serial]
fn testa_cmd_keys_clear_com_yes_true_limpa_todas_as_chaves() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
unsafe {
std::env::set_var("CONTEXT7_HOME", dir_temp.path());
}
escrever_config_xdg("ctx7sk-clear-alpha").expect("Deve escrever chave 1");
escrever_config_xdg("ctx7sk-clear-beta").expect("Deve escrever chave 2");
let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
let antes = ler_config_toml_do_caminho(&caminho).expect("Deve ler config antes");
assert_eq!(antes.keys.len(), 2, "Pré-condição: 2 chaves antes do clear");
cmd_keys_clear(true).expect("clear com yes=true deve funcionar");
let depois = ler_config_toml_do_caminho(&caminho).expect("Deve ler config depois");
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
assert!(
depois.keys.is_empty(),
"Após clear com yes=true, chaves devem estar vazias"
);
}
#[test]
#[serial_test::serial]
fn testa_cmd_keys_clear_com_yes_true_em_config_inexistente_funciona() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
unsafe {
std::env::set_var("CONTEXT7_HOME", dir_temp.path());
}
let resultado = cmd_keys_clear(true);
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
assert!(
resultado.is_ok(),
"clear em config inexistente deve retornar Ok (idempotente), obteve: {:?}",
resultado
);
}
#[test]
#[serial_test::serial]
fn testa_cmd_keys_import_env_valido_com_multiplas_chaves() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
let arquivo_env = dir_temp.path().join("chaves.env");
std::fs::write(
&arquivo_env,
"CONTEXT7_API=ctx7sk-import-alpha\nCONTEXT7_API=ctx7sk-import-beta\n",
)
.expect("Deve escrever .env de teste");
unsafe {
std::env::set_var("CONTEXT7_HOME", dir_temp.path());
}
let resultado = cmd_keys_import(&arquivo_env);
let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config após import");
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
resultado.expect("import de .env válido deve funcionar");
assert_eq!(config.keys.len(), 2, "Deve ter importado 2 chaves");
let valores: Vec<&str> = config.keys.iter().map(|c| c.value.as_str()).collect();
assert!(valores.contains(&"ctx7sk-import-alpha"));
assert!(valores.contains(&"ctx7sk-import-beta"));
}
#[test]
#[serial_test::serial]
fn testa_cmd_keys_import_env_sem_chaves_retorna_err() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
let arquivo_env = dir_temp.path().join("vazio.env");
std::fs::write(&arquivo_env, "# apenas comentario\nOUTRA_VAR=valor\n")
.expect("Deve escrever .env sem chaves");
unsafe {
std::env::set_var("CONTEXT7_HOME", dir_temp.path());
}
let resultado = cmd_keys_import(&arquivo_env);
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
assert!(
resultado.is_err(),
"Import de .env sem chaves CONTEXT7_API deve retornar Err"
);
}
#[test]
#[serial_test::serial]
fn testa_cmd_keys_import_arquivo_inexistente_retorna_err() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
let arquivo_inexistente = dir_temp.path().join("nao_existe.env");
unsafe {
std::env::set_var("CONTEXT7_HOME", dir_temp.path());
}
let resultado = cmd_keys_import(&arquivo_inexistente);
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
assert!(
resultado.is_err(),
"Import de arquivo inexistente deve retornar Err"
);
}
#[test]
#[serial_test::serial]
fn testa_cmd_keys_import_roundtrip_add_depois_list() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
let arquivo_env = dir_temp.path().join("roundtrip.env");
std::fs::write(
&arquivo_env,
"CONTEXT7_API=ctx7sk-rtrip-01\nCONTEXT7_API=ctx7sk-rtrip-02\n",
)
.expect("Deve escrever .env de roundtrip");
unsafe {
std::env::set_var("CONTEXT7_HOME", dir_temp.path());
}
cmd_keys_import(&arquivo_env).expect("Import deve funcionar");
let config = ler_config_xdg_raw()
.expect("Deve retornar Ok")
.expect("Deve retornar Some após import");
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
assert_eq!(
config.keys.len(),
2,
"Roundtrip: deve ter 2 chaves após import"
);
assert_eq!(config.keys[0].value, "ctx7sk-rtrip-01");
assert_eq!(config.keys[1].value, "ctx7sk-rtrip-02");
}
#[test]
#[serial_test::serial]
fn testa_cmd_keys_export_em_config_vazio_retorna_ok() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
unsafe {
std::env::set_var("CONTEXT7_HOME", dir_temp.path());
}
let resultado = cmd_keys_export();
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
assert!(
resultado.is_ok(),
"Export de config vazio deve retornar Ok, obteve: {:?}",
resultado
);
}
#[test]
#[serial_test::serial]
fn testa_cmd_keys_export_retorna_ok_com_chaves_existentes() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
unsafe {
std::env::set_var("CONTEXT7_HOME", dir_temp.path());
}
escrever_config_xdg("ctx7sk-export-um").expect("Deve escrever chave 1");
escrever_config_xdg("ctx7sk-export-dois").expect("Deve escrever chave 2");
let resultado = cmd_keys_export();
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
assert!(
resultado.is_ok(),
"Export com chaves existentes deve retornar Ok, obteve: {:?}",
resultado
);
}
#[test]
#[serial_test::serial]
fn testa_ler_config_xdg_raw_retorna_none_sem_arquivo() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
unsafe {
std::env::set_var("CONTEXT7_HOME", dir_temp.path());
}
let resultado = ler_config_xdg_raw();
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
let valor = resultado.expect("Deve retornar Ok");
assert!(
valor.is_none(),
"Deve retornar None quando config.toml não existe"
);
}
#[test]
#[serial_test::serial]
fn testa_ler_config_xdg_raw_retorna_config_com_chaves() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
let dir_context7 = dir_temp.path().join("context7");
std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
let toml = r#"schema_version = 1
[[keys]]
value = "ctx7sk-raw-01"
added_at = "2026-04-08T00:00:00+00:00"
[[keys]]
value = "ctx7sk-raw-02"
added_at = "2026-04-08T00:01:00+00:00"
"#;
std::fs::write(dir_context7.join("config.toml"), toml).expect("Deve escrever config.toml");
unsafe {
std::env::set_var("CONTEXT7_HOME", dir_temp.path());
}
let resultado = ler_config_xdg_raw();
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
let config = resultado
.expect("Deve retornar Ok")
.expect("Deve retornar Some com config");
assert_eq!(config.keys.len(), 2);
assert_eq!(config.keys[0].value, "ctx7sk-raw-01");
assert_eq!(config.keys[1].value, "ctx7sk-raw-02");
}
#[test]
#[serial_test::serial]
fn testa_cmd_keys_path_retorna_ok() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
unsafe {
std::env::set_var("CONTEXT7_HOME", dir_temp.path());
}
let resultado = cmd_keys_path();
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
resultado.expect("cmd_keys_path deve retornar Ok");
}
#[test]
#[serial_test::serial]
fn testa_descobrir_caminho_config_termina_com_config_toml() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
unsafe {
std::env::set_var("CONTEXT7_HOME", dir_temp.path());
}
let caminho = descobrir_caminho_config();
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
let caminho = caminho.expect("Deve retornar caminho XDG válido");
assert!(
caminho.to_string_lossy().ends_with("config.toml"),
"Caminho deve terminar com config.toml, obteve: {}",
caminho.display()
);
assert!(
caminho.to_string_lossy().contains("context7"),
"Caminho deve conter 'context7', obteve: {}",
caminho.display()
);
}
#[test]
#[serial_test::serial]
fn testa_fluxo_completo_add_list_remove_clear() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
unsafe {
std::env::set_var("CONTEXT7_HOME", dir_temp.path());
}
cmd_keys_add("ctx7sk-fluxo-01").expect("Add 1 deve funcionar");
cmd_keys_add("ctx7sk-fluxo-02").expect("Add 2 deve funcionar");
cmd_keys_add("ctx7sk-fluxo-03").expect("Add 3 deve funcionar");
let config_antes = ler_config_xdg_raw()
.expect("Ok")
.expect("Some com 3 chaves");
assert_eq!(config_antes.keys.len(), 3, "Deve ter 3 chaves após 3 adds");
cmd_keys_remove(2).expect("Remove índice 2 deve funcionar");
let config_pos_remove = ler_config_xdg_raw()
.expect("Ok")
.expect("Some com 2 chaves");
assert_eq!(
config_pos_remove.keys.len(),
2,
"Deve ter 2 chaves após remove"
);
assert_eq!(config_pos_remove.keys[0].value, "ctx7sk-fluxo-01");
assert_eq!(config_pos_remove.keys[1].value, "ctx7sk-fluxo-03");
cmd_keys_clear(true).expect("Clear com yes=true deve funcionar");
let caminho = descobrir_caminho_config().expect("Deve ter caminho");
let config_final = ler_config_toml_do_caminho(&caminho).expect("Deve ler config final");
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
assert!(
config_final.keys.is_empty(),
"Após clear, chaves devem estar vazias"
);
}
#[test]
#[serial_test::serial]
fn testa_context7_home_override_config_path() {
let tmp = tempfile::TempDir::new().expect("Deve criar tempdir");
unsafe {
std::env::set_var("CONTEXT7_HOME", tmp.path());
}
let caminho = descobrir_caminho_config();
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
let caminho = caminho.expect("Deve retornar Some quando CONTEXT7_HOME está definido");
let esperado = tmp.path().join("context7").join("config.toml");
assert_eq!(
caminho, esperado,
"CONTEXT7_HOME deve definir caminho como {{CONTEXT7_HOME}}/context7/config.toml"
);
}
#[test]
#[serial_test::serial]
fn testa_context7_home_override_logs_path() {
let tmp = tempfile::TempDir::new().expect("Deve criar tempdir");
unsafe {
std::env::set_var("CONTEXT7_HOME", tmp.path());
}
let caminho = descobrir_caminho_logs_xdg();
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
let caminho = caminho.expect("Deve retornar Some quando CONTEXT7_HOME está definido");
let esperado = tmp.path().join("context7").join("logs");
assert_eq!(
caminho, esperado,
"CONTEXT7_HOME deve definir logs como {{CONTEXT7_HOME}}/context7/logs"
);
}
#[test]
#[serial_test::serial]
fn testa_context7_home_vazio_cai_em_projectdirs() {
let tmp = tempfile::TempDir::new().expect("Deve criar tempdir");
unsafe {
std::env::set_var("CONTEXT7_HOME", "");
}
let caminho = descobrir_caminho_config();
unsafe {
std::env::remove_var("CONTEXT7_HOME");
}
if let Some(c) = caminho {
let tmp_str = tmp.path().to_string_lossy();
assert!(
!c.to_string_lossy().starts_with(tmp_str.as_ref()),
"CONTEXT7_HOME vazio não deve usar o tempdir: {}",
c.display()
);
}
}
}