use anyhow::{bail, Context, Result};
use chrono::Utc;
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[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>,
}
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(())
}
pub fn descobrir_caminho_config() -> Option<PathBuf> {
ProjectDirs::from("", "", "context7").map(|dirs| dirs.config_dir().join("config.toml"))
}
pub fn descobrir_caminho_logs_xdg() -> Option<PathBuf> {
ProjectDirs::from("", "", "context7").map(|dirs| {
#[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()
}
})
}
pub fn ler_env_var_chave() -> Option<Vec<String>> {
std::env::var("CONTEXT7_API_KEYS")
.ok()
.map(|valor| {
valor
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
})
.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| {
valor
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
})
}
pub fn carregar_chaves_api() -> Result<Vec<String>> {
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);
}
match ler_config_xdg() {
Ok(Some(chaves)) => {
info!("Chaves carregadas via configuração XDG");
return Ok(chaves);
}
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);
}
if let Some(chaves) = ler_env_compile_time() {
info!("Chaves carregadas via compile-time CONTEXT7_API_KEYS");
return Ok(chaves);
}
bail!("Nenhuma chave de API encontrada. Configure CONTEXT7_API_KEYS, ~/.config/context7/config.toml ou um arquivo .env com CONTEXT7_API=<chave>")
}
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>> {
use crate::errors::ErroContext7;
use anyhow::ensure;
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();
ensure!(!chaves.is_empty(), ErroContext7::SemChavesApi);
Ok(chaves)
}
pub fn confirmar_clear() -> Result<bool> {
use std::io::Write;
print!("Tem certeza que deseja remover TODAS as chaves? [s/N] ");
std::io::stdout()
.flush()
.context("Falha ao limpar buffer de saída")?;
let mut entrada = String::new();
std::io::stdin()
.read_line(&mut entrada)
.context("Falha ao ler confirmação do usuário")?;
Ok(matches!(
entrada.trim().to_lowercase().as_str(),
"s" | "sim"
))
}
pub fn cmd_keys_add(chave: &str) -> Result<()> {
let caminho = escrever_config_xdg(chave)?;
crate::output::exibir_chave_adicionada(&caminho);
Ok(())
}
pub fn cmd_keys_list() -> Result<()> {
match ler_config_xdg_raw()? {
None => crate::output::exibir_nenhuma_chave(),
Some(config) if config.keys.is_empty() => crate::output::exibir_nenhuma_chave(),
Some(config) => 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();
return Ok(());
}
Some(c) if c.keys.is_empty() => {
crate::output::exibir_nenhuma_chave_para_remover();
return Ok(());
}
Some(c) => c,
};
if indice == 0 || indice > config.keys.len() {
crate::output::exibir_indice_invalido(indice, config.keys.len());
return Ok(());
}
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 && !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) => println!("{}", caminho.display()),
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!(
"Nenhuma chave CONTEXT7_API= encontrada em: {}",
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 {
println!("CONTEXT7_API={}", 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_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_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("XDG_CONFIG_HOME", dir_temp.path());
}
let resultado = ler_config_xdg();
unsafe {
std::env::remove_var("XDG_CONFIG_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("XDG_CONFIG_HOME", dir_temp.path());
}
let resultado = ler_config_xdg();
unsafe {
std::env::remove_var("XDG_CONFIG_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("XDG_CONFIG_HOME", dir_temp.path());
}
let resultado = ler_config_xdg();
unsafe {
std::env::remove_var("XDG_CONFIG_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("XDG_CONFIG_HOME", dir_temp.path());
}
let resultado = ler_config_xdg();
unsafe {
std::env::remove_var("XDG_CONFIG_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("XDG_CONFIG_HOME", dir_temp.path());
}
let resultado = ler_config_xdg();
unsafe {
std::env::remove_var("XDG_CONFIG_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("XDG_CONFIG_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("XDG_CONFIG_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("XDG_CONFIG_HOME", &xdg_novo);
}
let resultado = escrever_config_xdg("ctx7sk-mkdir-teste");
unsafe {
std::env::remove_var("XDG_CONFIG_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("XDG_CONFIG_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("XDG_CONFIG_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("XDG_CONFIG_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("XDG_CONFIG_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("XDG_CONFIG_HOME", dir_temp.path());
}
let caminho =
escrever_config_xdg("ctx7sk-perm-600").expect("Deve escrever config sem erro");
unsafe {
std::env::remove_var("XDG_CONFIG_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("XDG_CONFIG_HOME", dir_temp.path());
}
let resultado = carregar_chaves_api();
unsafe {
std::env::remove_var("CONTEXT7_API_KEYS");
std::env::remove_var("XDG_CONFIG_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("XDG_CONFIG_HOME", dir_temp.path());
}
let resultado = carregar_chaves_api();
unsafe {
std::env::remove_var("XDG_CONFIG_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("XDG_CONFIG_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("XDG_CONFIG_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("XDG_CONFIG_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("XDG_CONFIG_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("XDG_CONFIG_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("XDG_CONFIG_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("XDG_CONFIG_HOME", dir_temp.path());
}
let resultado = cmd_keys_add("ctx7sk-nova-chave-add-test");
unsafe {
std::env::remove_var("XDG_CONFIG_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("XDG_CONFIG_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("XDG_CONFIG_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("XDG_CONFIG_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("XDG_CONFIG_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("XDG_CONFIG_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("XDG_CONFIG_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("XDG_CONFIG_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("XDG_CONFIG_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("XDG_CONFIG_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("XDG_CONFIG_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_ok_com_mensagem() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
unsafe {
std::env::set_var("XDG_CONFIG_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("XDG_CONFIG_HOME");
}
assert!(
resultado.is_ok(),
"Índice 0 inválido deve retornar Ok (não Err), obteve: {:?}",
resultado
);
}
#[test]
#[serial_test::serial]
fn testa_cmd_keys_remove_indice_maior_que_len_retorna_ok_com_mensagem() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
unsafe {
std::env::set_var("XDG_CONFIG_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("XDG_CONFIG_HOME");
}
assert!(
resultado.is_ok(),
"Índice fora do range deve retornar Ok (não Err), obteve: {:?}",
resultado
);
}
#[test]
#[serial_test::serial]
fn testa_cmd_keys_remove_em_config_vazio_retorna_ok() {
let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
unsafe {
std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
}
let resultado = cmd_keys_remove(1);
unsafe {
std::env::remove_var("XDG_CONFIG_HOME");
}
assert!(
resultado.is_ok(),
"Remover de config vazio deve retornar Ok, 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("XDG_CONFIG_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("XDG_CONFIG_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("XDG_CONFIG_HOME", dir_temp.path());
}
let resultado = cmd_keys_clear(true);
unsafe {
std::env::remove_var("XDG_CONFIG_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("XDG_CONFIG_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("XDG_CONFIG_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("XDG_CONFIG_HOME", dir_temp.path());
}
let resultado = cmd_keys_import(&arquivo_env);
unsafe {
std::env::remove_var("XDG_CONFIG_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("XDG_CONFIG_HOME", dir_temp.path());
}
let resultado = cmd_keys_import(&arquivo_inexistente);
unsafe {
std::env::remove_var("XDG_CONFIG_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("XDG_CONFIG_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("XDG_CONFIG_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("XDG_CONFIG_HOME", dir_temp.path());
}
let resultado = cmd_keys_export();
unsafe {
std::env::remove_var("XDG_CONFIG_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("XDG_CONFIG_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("XDG_CONFIG_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("XDG_CONFIG_HOME", dir_temp.path());
}
let resultado = ler_config_xdg_raw();
unsafe {
std::env::remove_var("XDG_CONFIG_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("XDG_CONFIG_HOME", dir_temp.path());
}
let resultado = ler_config_xdg_raw();
unsafe {
std::env::remove_var("XDG_CONFIG_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("XDG_CONFIG_HOME", dir_temp.path());
}
let resultado = cmd_keys_path();
unsafe {
std::env::remove_var("XDG_CONFIG_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("XDG_CONFIG_HOME", dir_temp.path());
}
let caminho = descobrir_caminho_config();
unsafe {
std::env::remove_var("XDG_CONFIG_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("XDG_CONFIG_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("XDG_CONFIG_HOME");
}
assert!(
config_final.keys.is_empty(),
"Após clear, chaves devem estar vazias"
);
}
}