1use anyhow::{bail, Context, Result};
9use chrono::Utc;
10use directories::ProjectDirs;
11use serde::{Deserialize, Serialize};
12use std::path::PathBuf;
13
14#[derive(Debug, Serialize, Deserialize, Clone)]
20pub struct ChaveArmazenada {
21 pub value: String,
23 pub added_at: String,
25}
26
27#[derive(Debug, Serialize, Deserialize, Default)]
31pub struct ConfigArquivo {
32 pub schema_version: u32,
34 #[serde(default)]
36 pub keys: Vec<ChaveArmazenada>,
37}
38
39pub fn aplicar_permissoes_600(caminho: &std::path::Path) -> Result<()> {
46 #[cfg(unix)]
47 {
48 use std::os::unix::fs::PermissionsExt;
49 let mut perms = std::fs::metadata(caminho)
50 .with_context(|| format!("Falha ao ler metadados de: {}", caminho.display()))?
51 .permissions();
52 perms.set_mode(0o600);
53 std::fs::set_permissions(caminho, perms)
54 .with_context(|| format!("Falha ao definir permissões em: {}", caminho.display()))?;
55 }
56 #[cfg(not(unix))]
57 let _ = caminho;
58 Ok(())
59}
60
61fn resolver_home_override() -> Option<PathBuf> {
69 let home = std::env::var("CONTEXT7_HOME").ok()?;
70 if home.is_empty() {
71 return None;
72 }
73 let base = PathBuf::from(&home);
74 if base
76 .components()
77 .any(|c| c == std::path::Component::ParentDir)
78 {
79 return None;
80 }
81 Some(base)
82}
83
84pub fn descobrir_caminho_config() -> Option<PathBuf> {
91 if let Some(base) = resolver_home_override() {
92 return Some(base.join("context7").join("config.toml"));
93 }
94 ProjectDirs::from("", "", "context7").map(|dirs| dirs.config_dir().join("config.toml"))
95}
96
97pub fn descobrir_caminho_logs_xdg() -> Option<PathBuf> {
104 if let Some(base) = resolver_home_override() {
105 return Some(base.join("context7").join("logs"));
106 }
107 ProjectDirs::from("", "", "context7").map(|dirs| {
108 #[cfg(target_os = "linux")]
110 {
111 dirs.state_dir()
112 .unwrap_or_else(|| dirs.data_local_dir())
113 .to_path_buf()
114 }
115 #[cfg(not(target_os = "linux"))]
116 {
117 dirs.data_local_dir().to_path_buf()
118 }
119 })
120}
121
122pub fn ler_env_var_chave() -> Option<Vec<String>> {
131 std::env::var("CONTEXT7_API_KEYS")
132 .ok()
133 .map(|valor| {
134 valor
135 .split(',')
136 .map(|s| s.trim().to_string())
137 .filter(|s| !s.is_empty())
138 .collect::<Vec<_>>()
139 })
140 .filter(|v| !v.is_empty())
141}
142
143pub fn ler_config_xdg() -> Result<Option<Vec<String>>> {
148 let caminho = match descobrir_caminho_config() {
149 Some(p) => p,
150 None => return Ok(None),
151 };
152
153 if !caminho.exists() {
154 return Ok(None);
155 }
156
157 let conteudo = std::fs::read_to_string(&caminho)
158 .with_context(|| format!("Falha ao ler configuração XDG em: {}", caminho.display()))?;
159
160 let config: ConfigArquivo = toml::from_str(&conteudo)
161 .with_context(|| format!("TOML inválido em: {}", caminho.display()))?;
162
163 let chaves: Vec<String> = config
164 .keys
165 .into_iter()
166 .map(|c| c.value)
167 .filter(|v| !v.is_empty())
168 .collect();
169
170 if chaves.is_empty() {
171 Ok(None)
172 } else {
173 Ok(Some(chaves))
174 }
175}
176
177pub fn ler_env_cwd() -> Option<Vec<String>> {
182 let caminho = std::env::current_dir().ok().map(|d| d.join(".env"))?;
183
184 if !caminho.exists() {
185 return None;
186 }
187
188 std::fs::read_to_string(&caminho)
189 .ok()
190 .and_then(|conteudo| extrair_chaves_env(&conteudo).ok())
191}
192
193pub fn ler_env_compile_time() -> Option<Vec<String>> {
204 option_env!("CONTEXT7_API_KEYS").map(|valor| {
205 valor
206 .split(',')
207 .map(|s| s.trim().to_string())
208 .filter(|s| !s.is_empty())
209 .collect()
210 })
211}
212
213pub fn carregar_chaves_api() -> Result<Vec<String>> {
222 use tracing::{info, warn};
223
224 if let Some(chaves) = ler_env_var_chave() {
226 info!("Chaves carregadas via variável de ambiente CONTEXT7_API_KEYS");
227 return Ok(chaves);
228 }
229
230 match ler_config_xdg() {
232 Ok(Some(chaves)) => {
233 info!("Chaves carregadas via configuração XDG");
234 return Ok(chaves);
235 }
236 Ok(None) => {}
237 Err(e) => {
238 warn!("Falha ao ler configuração XDG (continuando): {}", e);
239 }
240 }
241
242 if let Some(chaves) = ler_env_cwd() {
244 info!(
245 "Iniciando context7 com {} chaves de API disponíveis",
246 chaves.len()
247 );
248 return Ok(chaves);
249 }
250
251 if let Some(chaves) = ler_env_compile_time() {
253 info!("Chaves carregadas via compile-time CONTEXT7_API_KEYS");
254 return Ok(chaves);
255 }
256
257 bail!("Nenhuma chave de API encontrada. Configure CONTEXT7_API_KEYS, ~/.config/context7/config.toml ou um arquivo .env com CONTEXT7_API=<chave>")
258}
259
260pub fn escrever_config_xdg(nova_chave: &str) -> Result<PathBuf> {
267 let caminho = descobrir_caminho_config()
268 .context("Sistema não suporta diretórios XDG — impossível salvar configuração")?;
269
270 if let Some(pai) = caminho.parent() {
272 std::fs::create_dir_all(pai)
273 .with_context(|| format!("Falha ao criar diretório: {}", pai.display()))?;
274 }
275
276 let mut config = if caminho.exists() {
278 let conteudo = std::fs::read_to_string(&caminho)
279 .with_context(|| format!("Falha ao ler config existente: {}", caminho.display()))?;
280 toml::from_str::<ConfigArquivo>(&conteudo)
281 .with_context(|| format!("TOML inválido em: {}", caminho.display()))?
282 } else {
283 ConfigArquivo {
284 schema_version: 1,
285 keys: Vec::new(),
286 }
287 };
288
289 let ja_existe = config.keys.iter().any(|c| c.value == nova_chave);
291 if !ja_existe {
292 config.keys.push(ChaveArmazenada {
293 value: nova_chave.to_string(),
294 added_at: Utc::now().to_rfc3339(),
295 });
296 }
297
298 let toml_str =
300 toml::to_string_pretty(&config).context("Falha ao serializar configuração para TOML")?;
301 std::fs::write(&caminho, &toml_str)
302 .with_context(|| format!("Falha ao escrever config em: {}", caminho.display()))?;
303
304 aplicar_permissoes_600(&caminho)?;
305
306 Ok(caminho)
307}
308
309pub fn ler_config_xdg_raw() -> Result<Option<ConfigArquivo>> {
314 let caminho = match descobrir_caminho_config() {
315 Some(p) => p,
316 None => return Ok(None),
317 };
318
319 if !caminho.exists() {
320 return Ok(None);
321 }
322
323 let conteudo = std::fs::read_to_string(&caminho)
324 .with_context(|| format!("Falha ao ler configuração XDG em: {}", caminho.display()))?;
325
326 let config: ConfigArquivo = toml::from_str(&conteudo)
327 .with_context(|| format!("TOML inválido em: {}", caminho.display()))?;
328
329 Ok(Some(config))
330}
331
332pub fn escrever_config_arquivo(config: &ConfigArquivo) -> Result<PathBuf> {
337 let caminho = descobrir_caminho_config()
338 .context("Sistema não suporta diretórios XDG — impossível salvar configuração")?;
339
340 if let Some(pai) = caminho.parent() {
341 std::fs::create_dir_all(pai)
342 .with_context(|| format!("Falha ao criar diretório: {}", pai.display()))?;
343 }
344
345 let toml_str =
346 toml::to_string_pretty(config).context("Falha ao serializar configuração para TOML")?;
347 std::fs::write(&caminho, &toml_str)
348 .with_context(|| format!("Falha ao escrever config em: {}", caminho.display()))?;
349
350 aplicar_permissoes_600(&caminho)?;
351
352 Ok(caminho)
353}
354
355pub fn mascarar_chave(chave: &str) -> String {
364 let n_chars = chave.chars().count();
365 let inicio = 12;
366 let fim = 4;
367 if n_chars <= inicio + fim {
368 return "***".to_string();
369 }
370 let prefixo: String = chave.chars().take(inicio).collect();
371 let sufixo: String = chave
372 .chars()
373 .rev()
374 .take(fim)
375 .collect::<String>()
376 .chars()
377 .rev()
378 .collect();
379 format!("{}...{}", prefixo, sufixo)
380}
381
382pub fn extrair_chaves_env(conteudo: &str) -> Result<Vec<String>> {
388 use crate::errors::ErroContext7;
389 use anyhow::ensure;
390
391 let chaves: Vec<String> = conteudo
392 .lines()
393 .filter_map(|linha| {
394 let linha_sem_comentario = linha.split('#').next().unwrap_or("").trim();
396 linha_sem_comentario
397 .strip_prefix("CONTEXT7_API=")
398 .map(|valor| {
399 valor
401 .trim()
402 .trim_matches('"')
403 .trim_matches('\'')
404 .to_string()
405 })
406 .filter(|v| !v.is_empty())
407 })
408 .collect();
409
410 ensure!(!chaves.is_empty(), ErroContext7::SemChavesApi);
411
412 Ok(chaves)
413}
414
415pub fn confirmar_clear() -> Result<bool> {
419 use std::io::Write;
420 print!("Tem certeza que deseja remover TODAS as chaves? [s/N] ");
421 std::io::stdout()
422 .flush()
423 .context("Falha ao limpar buffer de saída")?;
424
425 let mut entrada = String::new();
426 std::io::stdin()
427 .read_line(&mut entrada)
428 .context("Falha ao ler confirmação do usuário")?;
429
430 Ok(matches!(
431 entrada.trim().to_lowercase().as_str(),
432 "s" | "sim" | "y" | "yes"
433 ))
434}
435
436pub fn cmd_keys_add(chave: &str) -> Result<()> {
442 let caminho = escrever_config_xdg(chave)?;
443 crate::output::exibir_chave_adicionada(&caminho);
444 Ok(())
445}
446
447pub fn cmd_keys_list() -> Result<()> {
449 match ler_config_xdg_raw()? {
450 None => crate::output::exibir_nenhuma_chave(),
451 Some(config) if config.keys.is_empty() => crate::output::exibir_nenhuma_chave(),
452 Some(config) => crate::output::exibir_chaves_mascaradas(&config.keys, mascarar_chave),
453 }
454 Ok(())
455}
456
457pub fn cmd_keys_remove(indice: usize) -> Result<()> {
459 let mut config = match ler_config_xdg_raw()? {
460 None => {
461 crate::output::exibir_nenhuma_chave_para_remover();
462 return Ok(());
463 }
464 Some(c) if c.keys.is_empty() => {
465 crate::output::exibir_nenhuma_chave_para_remover();
466 return Ok(());
467 }
468 Some(c) => c,
469 };
470
471 if indice == 0 || indice > config.keys.len() {
472 crate::output::exibir_indice_invalido(indice, config.keys.len());
473 return Ok(());
474 }
475
476 let removida = config.keys.remove(indice - 1);
477 escrever_config_arquivo(&config)?;
478 crate::output::exibir_chave_removida(&mascarar_chave(&removida.value));
479 Ok(())
480}
481
482pub fn cmd_keys_clear(sim: bool) -> Result<()> {
484 if !sim && !confirmar_clear()? {
485 crate::output::exibir_operacao_cancelada();
486 return Ok(());
487 }
488
489 let config = ConfigArquivo {
490 schema_version: 1,
491 keys: Vec::new(),
492 };
493 escrever_config_arquivo(&config)?;
494 crate::output::exibir_chaves_removidas();
495 Ok(())
496}
497
498pub fn cmd_keys_path() -> Result<()> {
500 match descobrir_caminho_config() {
501 Some(caminho) => println!("{}", caminho.display()),
502 None => crate::output::exibir_xdg_nao_suportado(),
503 }
504 Ok(())
505}
506
507pub fn cmd_keys_import(arquivo: &std::path::Path) -> Result<()> {
511 let conteudo = std::fs::read_to_string(arquivo)
512 .with_context(|| format!("Falha ao ler arquivo: {}", arquivo.display()))?;
513
514 let chaves = extrair_chaves_env(&conteudo).with_context(|| {
515 format!(
516 "Nenhuma chave CONTEXT7_API= encontrada em: {}",
517 arquivo.display()
518 )
519 })?;
520
521 let total = chaves.len();
522 let mut importadas = 0usize;
523
524 for chave in &chaves {
525 escrever_config_xdg(chave)?;
526 importadas += 1;
527 }
528
529 crate::output::exibir_importacao_concluida(importadas, total);
530 Ok(())
531}
532
533pub fn cmd_keys_export() -> Result<()> {
537 match ler_config_xdg_raw()? {
538 None => {}
539 Some(config) if config.keys.is_empty() => {}
540 Some(config) => {
541 for chave in &config.keys {
542 println!("CONTEXT7_API={}", chave.value);
543 }
544 }
545 }
546 Ok(())
547}
548
549#[cfg(test)]
552mod testes {
553 use super::*;
554
555 fn ler_config_toml_do_caminho(caminho: &std::path::Path) -> Result<ConfigArquivo> {
559 let conteudo = std::fs::read_to_string(caminho)
560 .with_context(|| format!("Falha ao ler: {}", caminho.display()))?;
561 toml::from_str(&conteudo)
562 .with_context(|| format!("TOML inválido em: {}", caminho.display()))
563 }
564
565 #[test]
568 fn testa_parsing_env_com_multiplas_chaves_iguais() {
569 let mut conteudo = String::new();
570 for i in 0..17 {
571 conteudo.push_str(&format!("CONTEXT7_API=ctx7sk-chave-{:02}\n", i));
572 }
573 let chaves = extrair_chaves_env(&conteudo).expect("Deve extrair 17 chaves sem erro");
574 assert_eq!(chaves.len(), 17, "Deve retornar exatamente 17 chaves");
575 for (i, chave) in chaves.iter().enumerate() {
576 assert_eq!(
577 chave,
578 &format!("ctx7sk-chave-{:02}", i),
579 "Chave {} deve ter o valor correto",
580 i
581 );
582 }
583 }
584
585 #[test]
586 fn testa_parsing_env_ignora_comentarios_e_linhas_vazias() {
587 let conteudo = "# Este é um comentário\n\
588 CONTEXT7_API=ctx7sk-chave-valida-01\n\
589 \n\
590 # Outro comentário\n\
591 CONTEXT7_API=ctx7sk-chave-valida-02\n\
592 \n";
593 let chaves = extrair_chaves_env(conteudo).expect("Deve extrair chaves sem erro");
594 assert_eq!(chaves.len(), 2, "Deve ignorar comentários e linhas vazias");
595 assert_eq!(chaves[0], "ctx7sk-chave-valida-01");
596 assert_eq!(chaves[1], "ctx7sk-chave-valida-02");
597 }
598
599 #[test]
600 fn testa_parsing_env_remove_aspas_duplas() {
601 let conteudo = "CONTEXT7_API=\"ctx7sk-abc-com-aspas\"\n";
602 let chaves = extrair_chaves_env(conteudo).expect("Deve extrair chave sem erro");
603 assert_eq!(chaves.len(), 1);
604 assert_eq!(
605 chaves[0], "ctx7sk-abc-com-aspas",
606 "Deve remover aspas duplas"
607 );
608 }
609
610 #[test]
611 fn testa_parsing_env_remove_aspas_simples() {
612 let conteudo = "CONTEXT7_API='ctx7sk-abc-aspas-simples'\n";
613 let chaves = extrair_chaves_env(conteudo).expect("Deve extrair chave sem erro");
614 assert_eq!(chaves.len(), 1);
615 assert_eq!(
616 chaves[0], "ctx7sk-abc-aspas-simples",
617 "Deve remover aspas simples"
618 );
619 }
620
621 #[test]
622 fn testa_parsing_env_erro_quando_nenhuma_chave() {
623 let conteudo = "# Apenas comentários\n\
624 OUTRA_VAR=valor\n\
625 \n";
626 let resultado = extrair_chaves_env(conteudo);
627 assert!(
628 resultado.is_err(),
629 "Deve retornar Err quando não há chaves CONTEXT7_API"
630 );
631 let mensagem_erro = resultado.unwrap_err().to_string();
632 assert!(
633 mensagem_erro.contains("chave")
634 || mensagem_erro.contains("CONTEXT7_API")
635 || mensagem_erro.contains("key")
636 || mensagem_erro.contains("API"),
637 "Mensagem de erro deve mencionar CONTEXT7_API, chave, key ou API, obteve: {}",
638 mensagem_erro
639 );
640 }
641
642 #[test]
643 fn testa_parsing_env_ignora_chaves_vazias() {
644 let conteudo = "CONTEXT7_API=\n\
645 CONTEXT7_API=ctx7sk-valida\n";
646 let chaves = extrair_chaves_env(conteudo).expect("Deve extrair chave sem erro");
647 assert_eq!(
648 chaves.len(),
649 1,
650 "Deve ignorar entradas CONTEXT7_API sem valor"
651 );
652 assert_eq!(chaves[0], "ctx7sk-valida");
653 }
654
655 #[test]
656 fn testa_parsing_env_ignora_comentario_inline() {
657 let conteudo = "CONTEXT7_API=ctx7sk-valida # comentário aqui\n";
658 let chaves = extrair_chaves_env(conteudo).expect("Deve extrair chave sem erro");
659 assert_eq!(chaves.len(), 1);
660 assert_eq!(chaves[0], "ctx7sk-valida");
661 }
662
663 #[test]
666 fn testa_mascarar_chave_com_valor_longo_exibe_prefixo_e_sufixo() {
667 let chave = "ctx7sk-abc123-def456-ghi789";
668 assert_eq!(chave.len(), 27, "Pré-condição: chave deve ter 27 chars");
669 let mascarada = mascarar_chave(chave);
670 assert!(
671 mascarada.starts_with("ctx7sk-abc12"),
672 "Deve iniciar com os primeiros 12 chars, obteve: {}",
673 mascarada
674 );
675 assert!(
676 mascarada.ends_with("i789"),
677 "Deve terminar com os últimos 4 chars, obteve: {}",
678 mascarada
679 );
680 assert!(
681 mascarada.contains("..."),
682 "Deve conter '...' entre prefixo e sufixo, obteve: {}",
683 mascarada
684 );
685 }
686
687 #[test]
688 fn testa_mascarar_chave_curta_retorna_asteriscos() {
689 let chave_exatamente_16 = "ctx7sk-abcdef012";
690 assert_eq!(
691 chave_exatamente_16.len(),
692 16,
693 "Pré-condição: chave deve ter 16 chars"
694 );
695 let mascarada = mascarar_chave(chave_exatamente_16);
696 assert_eq!(
697 mascarada, "***",
698 "Chave de 16 chars deve retornar '***', obteve: {}",
699 mascarada
700 );
701 }
702
703 #[test]
704 fn testa_mascarar_chave_vazia_retorna_asteriscos() {
705 let mascarada = mascarar_chave("");
706 assert_eq!(
707 mascarada, "***",
708 "Chave vazia deve retornar '***', obteve: {}",
709 mascarada
710 );
711 }
712
713 #[test]
714 fn testa_mascarar_chave_de_exatamente_17_chars_mascara_corretamente() {
715 let chave = "ctx7sk-abcdef0123"; assert_eq!(chave.len(), 17, "Pré-condição: chave deve ter 17 chars");
717 let mascarada = mascarar_chave(chave);
718 assert!(
719 mascarada.contains("..."),
720 "Chave de 17 chars deve ser mascarada, obteve: {}",
721 mascarada
722 );
723 assert_eq!(
724 &mascarada[..12],
725 &chave[..12],
726 "Prefixo de 12 chars deve ser preservado"
727 );
728 assert!(
729 mascarada.ends_with(&chave[chave.len() - 4..]),
730 "Sufixo de 4 chars deve ser preservado"
731 );
732 }
733
734 #[test]
737 #[serial_test::serial]
738 fn testa_ler_env_var_chave_retorna_some_quando_setada() {
739 unsafe {
742 std::env::set_var("CONTEXT7_API_KEYS", "ctx7sk-chave-teste-01");
743 }
744 let resultado = ler_env_var_chave();
745 unsafe {
746 std::env::remove_var("CONTEXT7_API_KEYS");
747 }
748
749 let chaves = resultado.expect("Deve retornar Some com chave válida");
750 assert_eq!(chaves.len(), 1, "Deve retornar exatamente 1 chave");
751 assert_eq!(chaves[0], "ctx7sk-chave-teste-01");
752 }
753
754 #[test]
755 #[serial_test::serial]
756 fn testa_ler_env_var_chave_aceita_multiplas_separadas_por_virgula() {
757 unsafe {
759 std::env::set_var(
760 "CONTEXT7_API_KEYS",
761 "ctx7sk-chave-a, ctx7sk-chave-b , ctx7sk-chave-c",
762 );
763 }
764 let resultado = ler_env_var_chave();
765 unsafe {
766 std::env::remove_var("CONTEXT7_API_KEYS");
767 }
768
769 let chaves = resultado.expect("Deve retornar Some com múltiplas chaves");
770 assert_eq!(chaves.len(), 3, "Deve retornar 3 chaves");
771 assert_eq!(chaves[0], "ctx7sk-chave-a");
772 assert_eq!(chaves[1], "ctx7sk-chave-b");
773 assert_eq!(chaves[2], "ctx7sk-chave-c");
774 }
775
776 #[test]
777 #[serial_test::serial]
778 fn testa_ler_env_var_chave_retorna_none_quando_vazia() {
779 unsafe {
781 std::env::set_var("CONTEXT7_API_KEYS", "");
782 }
783 let resultado = ler_env_var_chave();
784 unsafe {
785 std::env::remove_var("CONTEXT7_API_KEYS");
786 }
787
788 assert!(
789 resultado.is_none(),
790 "Deve retornar None quando env var está vazia"
791 );
792 }
793
794 #[test]
795 #[serial_test::serial]
796 fn testa_ler_env_var_chave_retorna_none_quando_apenas_whitespace() {
797 unsafe {
799 std::env::set_var("CONTEXT7_API_KEYS", " , , ");
800 }
801 let resultado = ler_env_var_chave();
802 unsafe {
803 std::env::remove_var("CONTEXT7_API_KEYS");
804 }
805
806 assert!(
807 resultado.is_none(),
808 "Deve retornar None quando env var contém apenas whitespace/vírgulas"
809 );
810 }
811
812 #[test]
813 #[serial_test::serial]
814 fn testa_ler_env_var_chave_retorna_none_quando_ausente() {
815 unsafe {
817 std::env::remove_var("CONTEXT7_API_KEYS");
818 }
819 let resultado = ler_env_var_chave();
820
821 assert!(
822 resultado.is_none(),
823 "Deve retornar None quando env var não existe"
824 );
825 }
826
827 #[test]
830 #[serial_test::serial]
831 fn testa_ler_config_xdg_arquivo_inexistente_retorna_none() {
832 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
833 unsafe {
835 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
836 }
837 let resultado = ler_config_xdg();
838 unsafe {
839 std::env::remove_var("CONTEXT7_HOME");
840 }
841
842 let valor = resultado.expect("Deve retornar Ok quando arquivo não existe");
843 assert!(
844 valor.is_none(),
845 "Deve retornar None quando config.toml não existe"
846 );
847 }
848
849 #[test]
850 #[serial_test::serial]
851 fn testa_ler_config_xdg_le_toml_valido_com_multiplas_chaves() {
852 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
853 let dir_context7 = dir_temp.path().join("context7");
854 std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
855
856 let toml_conteudo = r#"schema_version = 1
857
858[[keys]]
859value = "ctx7sk-chave-xdg-01"
860added_at = "2026-01-01T00:00:00+00:00"
861
862[[keys]]
863value = "ctx7sk-chave-xdg-02"
864added_at = "2026-01-02T00:00:00+00:00"
865"#;
866 std::fs::write(dir_context7.join("config.toml"), toml_conteudo)
867 .expect("Deve escrever config.toml");
868
869 unsafe {
871 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
872 }
873 let resultado = ler_config_xdg();
874 unsafe {
875 std::env::remove_var("CONTEXT7_HOME");
876 }
877
878 let chaves = resultado
879 .expect("Deve retornar Ok")
880 .expect("Deve retornar Some com chaves");
881 assert_eq!(chaves.len(), 2, "Deve retornar 2 chaves");
882 assert_eq!(chaves[0], "ctx7sk-chave-xdg-01");
883 assert_eq!(chaves[1], "ctx7sk-chave-xdg-02");
884 }
885
886 #[test]
887 #[serial_test::serial]
888 fn testa_ler_config_xdg_retorna_err_quando_toml_invalido() {
889 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
890 let dir_context7 = dir_temp.path().join("context7");
891 std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
892
893 std::fs::write(
894 dir_context7.join("config.toml"),
895 "schema_version = INVALIDO\n[[[malformado",
896 )
897 .expect("Deve escrever TOML inválido");
898
899 unsafe {
901 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
902 }
903 let resultado = ler_config_xdg();
904 unsafe {
905 std::env::remove_var("CONTEXT7_HOME");
906 }
907
908 assert!(
909 resultado.is_err(),
910 "Deve retornar Err quando TOML está malformado"
911 );
912 }
913
914 #[test]
915 #[serial_test::serial]
916 fn testa_ler_config_xdg_preserva_ordem_das_chaves() {
917 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
918 let dir_context7 = dir_temp.path().join("context7");
919 std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
920
921 let toml_conteudo = r#"schema_version = 1
922
923[[keys]]
924value = "ctx7sk-primeira"
925added_at = "2026-01-01T00:00:00+00:00"
926
927[[keys]]
928value = "ctx7sk-segunda"
929added_at = "2026-01-02T00:00:00+00:00"
930
931[[keys]]
932value = "ctx7sk-terceira"
933added_at = "2026-01-03T00:00:00+00:00"
934"#;
935 std::fs::write(dir_context7.join("config.toml"), toml_conteudo)
936 .expect("Deve escrever config.toml");
937
938 unsafe {
940 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
941 }
942 let resultado = ler_config_xdg();
943 unsafe {
944 std::env::remove_var("CONTEXT7_HOME");
945 }
946
947 let chaves = resultado
948 .expect("Deve retornar Ok")
949 .expect("Deve retornar Some");
950 assert_eq!(chaves[0], "ctx7sk-primeira");
951 assert_eq!(chaves[1], "ctx7sk-segunda");
952 assert_eq!(chaves[2], "ctx7sk-terceira");
953 }
954
955 #[test]
956 #[serial_test::serial]
957 fn testa_ler_config_xdg_keys_vazio_retorna_none() {
958 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
959 let dir_context7 = dir_temp.path().join("context7");
960 std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
961
962 let toml_sem_chaves = "schema_version = 1\n";
963 std::fs::write(dir_context7.join("config.toml"), toml_sem_chaves)
964 .expect("Deve escrever config.toml sem keys");
965
966 unsafe {
968 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
969 }
970 let resultado = ler_config_xdg();
971 unsafe {
972 std::env::remove_var("CONTEXT7_HOME");
973 }
974
975 let valor = resultado.expect("Deve retornar Ok");
976 assert!(
977 valor.is_none(),
978 "Deve retornar None quando config.toml existe mas keys está vazio"
979 );
980 }
981
982 #[test]
985 #[serial_test::serial]
986 fn testa_escrever_config_xdg_roundtrip_serializa_e_deserializa() {
987 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
988 unsafe {
990 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
991 }
992
993 let caminho =
994 escrever_config_xdg("ctx7sk-roundtrip-01").expect("Deve escrever config sem erro");
995
996 let config_lido = ler_config_toml_do_caminho(&caminho)
997 .expect("Deve ler TOML escrito por escrever_config_xdg");
998
999 unsafe {
1000 std::env::remove_var("CONTEXT7_HOME");
1001 }
1002
1003 assert_eq!(config_lido.schema_version, 1, "schema_version deve ser 1");
1004 assert_eq!(config_lido.keys.len(), 1, "Deve conter 1 chave");
1005 assert_eq!(
1006 config_lido.keys[0].value, "ctx7sk-roundtrip-01",
1007 "Valor da chave deve ser preservado"
1008 );
1009 assert!(
1010 !config_lido.keys[0].added_at.is_empty(),
1011 "added_at não deve ser vazio"
1012 );
1013 }
1014
1015 #[test]
1016 #[serial_test::serial]
1017 fn testa_escrever_config_xdg_cria_diretorios_pai_se_nao_existirem() {
1018 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1019 let xdg_novo = dir_temp.path().join("xdg_inexistente");
1020 unsafe {
1022 std::env::set_var("CONTEXT7_HOME", &xdg_novo);
1023 }
1024
1025 let resultado = escrever_config_xdg("ctx7sk-mkdir-teste");
1026 unsafe {
1027 std::env::remove_var("CONTEXT7_HOME");
1028 }
1029
1030 let caminho = resultado.expect("Deve criar diretório pai e escrever config");
1031 assert!(
1032 caminho.exists(),
1033 "Arquivo de config deve existir após escrita"
1034 );
1035 }
1036
1037 #[test]
1038 #[serial_test::serial]
1039 fn testa_escrever_config_xdg_nao_duplica_chave_ja_existente() {
1040 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1041 unsafe {
1043 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1044 }
1045
1046 escrever_config_xdg("ctx7sk-unica").expect("Primeira escrita deve funcionar");
1047 escrever_config_xdg("ctx7sk-unica").expect("Segunda escrita não deve falhar");
1048
1049 let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1050 let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config");
1051
1052 unsafe {
1053 std::env::remove_var("CONTEXT7_HOME");
1054 }
1055
1056 assert_eq!(
1057 config.keys.len(),
1058 1,
1059 "Não deve duplicar chave já existente — deve ter apenas 1"
1060 );
1061 }
1062
1063 #[test]
1064 #[serial_test::serial]
1065 fn testa_escrever_config_xdg_acumula_chaves_distintas() {
1066 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1067 unsafe {
1069 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1070 }
1071
1072 escrever_config_xdg("ctx7sk-chave-a").expect("Primeira escrita deve funcionar");
1073 escrever_config_xdg("ctx7sk-chave-b").expect("Segunda escrita deve funcionar");
1074 escrever_config_xdg("ctx7sk-chave-c").expect("Terceira escrita deve funcionar");
1075
1076 let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1077 let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config");
1078
1079 unsafe {
1080 std::env::remove_var("CONTEXT7_HOME");
1081 }
1082
1083 assert_eq!(config.keys.len(), 3, "Deve acumular 3 chaves distintas");
1084 let valores: Vec<&str> = config.keys.iter().map(|c| c.value.as_str()).collect();
1085 assert!(valores.contains(&"ctx7sk-chave-a"));
1086 assert!(valores.contains(&"ctx7sk-chave-b"));
1087 assert!(valores.contains(&"ctx7sk-chave-c"));
1088 }
1089
1090 #[test]
1091 #[cfg(unix)]
1092 #[serial_test::serial]
1093 fn testa_escrever_config_xdg_aplica_permissoes_600_em_unix() {
1094 use std::os::unix::fs::PermissionsExt;
1095
1096 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1097 unsafe {
1099 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1100 }
1101
1102 let caminho =
1103 escrever_config_xdg("ctx7sk-perm-600").expect("Deve escrever config sem erro");
1104 unsafe {
1105 std::env::remove_var("CONTEXT7_HOME");
1106 }
1107
1108 let metadados = std::fs::metadata(&caminho).expect("Deve obter metadados do arquivo");
1109 let modo = metadados.permissions().mode() & 0o777;
1110
1111 assert_eq!(modo, 0o600, "Permissões devem ser 600, obteve: {:o}", modo);
1112 }
1113
1114 #[test]
1117 fn testa_config_arquivo_roundtrip_serde_preserva_todos_campos() {
1118 let config_original = ConfigArquivo {
1119 schema_version: 1,
1120 keys: vec![
1121 ChaveArmazenada {
1122 value: "ctx7sk-serde-01".to_string(),
1123 added_at: "2026-01-01T12:00:00+00:00".to_string(),
1124 },
1125 ChaveArmazenada {
1126 value: "ctx7sk-serde-02".to_string(),
1127 added_at: "2026-01-02T12:00:00+00:00".to_string(),
1128 },
1129 ],
1130 };
1131
1132 let toml_str = toml::to_string_pretty(&config_original)
1133 .expect("Deve serializar ConfigArquivo para TOML");
1134 let config_deserializado: ConfigArquivo =
1135 toml::from_str(&toml_str).expect("Deve deserializar TOML de volta para ConfigArquivo");
1136
1137 assert_eq!(
1138 config_deserializado.schema_version, config_original.schema_version,
1139 "schema_version deve ser preservado no roundtrip"
1140 );
1141 assert_eq!(
1142 config_deserializado.keys.len(),
1143 config_original.keys.len(),
1144 "Número de chaves deve ser preservado"
1145 );
1146 assert_eq!(
1147 config_deserializado.keys[0].value, config_original.keys[0].value,
1148 "Valor da primeira chave deve ser preservado"
1149 );
1150 assert_eq!(
1151 config_deserializado.keys[0].added_at, config_original.keys[0].added_at,
1152 "added_at da primeira chave deve ser preservado"
1153 );
1154 }
1155
1156 #[test]
1157 fn testa_config_arquivo_schema_version_sempre_presente_na_serializacao() {
1158 let config = ConfigArquivo {
1159 schema_version: 1,
1160 keys: Vec::new(),
1161 };
1162
1163 let toml_str = toml::to_string_pretty(&config).expect("Deve serializar para TOML");
1164
1165 assert!(
1166 toml_str.contains("schema_version"),
1167 "schema_version deve estar presente na serialização TOML"
1168 );
1169 assert!(toml_str.contains('1'), "Valor 1 deve estar presente");
1170 }
1171
1172 #[test]
1173 fn testa_config_arquivo_keys_vazio_aceito_na_deserializacao() {
1174 let toml_str = "schema_version = 1\n";
1175 let config: ConfigArquivo =
1176 toml::from_str(toml_str).expect("Deve deserializar com keys ausente (default vazio)");
1177
1178 assert_eq!(config.schema_version, 1);
1179 assert!(
1180 config.keys.is_empty(),
1181 "keys deve ser vazio quando não presente no TOML"
1182 );
1183 }
1184
1185 #[test]
1186 fn testa_chave_armazenada_preserva_added_at_como_string_utc() {
1187 let timestamp = "2026-04-08T20:00:00+00:00";
1188 let chave = ChaveArmazenada {
1189 value: "ctx7sk-timestamp".to_string(),
1190 added_at: timestamp.to_string(),
1191 };
1192
1193 let toml_str = toml::to_string_pretty(&chave).expect("Deve serializar ChaveArmazenada");
1194 let chave_de_volta: ChaveArmazenada =
1195 toml::from_str(&toml_str).expect("Deve deserializar ChaveArmazenada");
1196
1197 assert_eq!(
1198 chave_de_volta.added_at, timestamp,
1199 "Timestamp added_at deve ser preservado exatamente"
1200 );
1201 }
1202
1203 #[test]
1206 #[serial_test::serial]
1207 fn testa_carregar_chaves_api_env_var_tem_prioridade_sobre_xdg() {
1208 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1209 let dir_context7 = dir_temp.path().join("context7");
1210 std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
1211
1212 let toml_xdg = r#"schema_version = 1
1213[[keys]]
1214value = "ctx7sk-xdg-deve-ser-ignorada"
1215added_at = "2026-01-01T00:00:00+00:00"
1216"#;
1217 std::fs::write(dir_context7.join("config.toml"), toml_xdg)
1218 .expect("Deve escrever config XDG");
1219
1220 unsafe {
1222 std::env::set_var("CONTEXT7_API_KEYS", "ctx7sk-env-var-prioritaria");
1223 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1224 }
1225
1226 let resultado = carregar_chaves_api();
1227
1228 unsafe {
1229 std::env::remove_var("CONTEXT7_API_KEYS");
1230 std::env::remove_var("CONTEXT7_HOME");
1231 }
1232
1233 let chaves = resultado.expect("Deve carregar chaves via env var");
1234 assert_eq!(chaves.len(), 1);
1235 assert_eq!(
1236 chaves[0], "ctx7sk-env-var-prioritaria",
1237 "Env var deve ter prioridade sobre XDG"
1238 );
1239 }
1240
1241 #[test]
1242 #[serial_test::serial]
1243 fn testa_carregar_chaves_api_xdg_usado_quando_env_var_ausente() {
1244 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1245 let dir_context7 = dir_temp.path().join("context7");
1246 std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
1247
1248 let toml_xdg = r#"schema_version = 1
1249[[keys]]
1250value = "ctx7sk-via-xdg"
1251added_at = "2026-01-01T00:00:00+00:00"
1252"#;
1253 std::fs::write(dir_context7.join("config.toml"), toml_xdg)
1254 .expect("Deve escrever config XDG");
1255
1256 unsafe {
1258 std::env::remove_var("CONTEXT7_API_KEYS");
1259 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1260 }
1261
1262 let resultado = carregar_chaves_api();
1263
1264 unsafe {
1265 std::env::remove_var("CONTEXT7_HOME");
1266 }
1267
1268 let chaves = resultado.expect("Deve carregar chaves via XDG");
1269 assert_eq!(chaves.len(), 1);
1270 assert_eq!(chaves[0], "ctx7sk-via-xdg");
1271 }
1272
1273 #[test]
1274 #[serial_test::serial]
1275 fn testa_carregar_chaves_api_retorna_err_quando_nada_disponivel() {
1276 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1277 let dir_xdg_vazio = dir_temp.path().join("xdg_vazio");
1278 std::fs::create_dir_all(&dir_xdg_vazio).expect("Deve criar diretório XDG vazio");
1279
1280 let dir_sem_env = dir_temp.path().join("sem_env");
1281 std::fs::create_dir_all(&dir_sem_env).expect("Deve criar diretório sem .env");
1282
1283 unsafe {
1285 std::env::remove_var("CONTEXT7_API_KEYS");
1286 std::env::set_var("CONTEXT7_HOME", &dir_xdg_vazio);
1287 }
1288 let cwd_original = std::env::current_dir().expect("Deve obter CWD atual");
1289 std::env::set_current_dir(&dir_sem_env).expect("Deve mudar CWD");
1290
1291 let resultado = carregar_chaves_api();
1292
1293 std::env::set_current_dir(&cwd_original).expect("Deve restaurar CWD");
1294 unsafe {
1295 std::env::remove_var("CONTEXT7_HOME");
1296 }
1297
1298 assert!(
1299 resultado.is_err(),
1300 "Deve retornar Err quando nenhuma camada fornecer chaves"
1301 );
1302 }
1303
1304 #[test]
1305 #[serial_test::serial]
1306 fn testa_ler_env_cwd_le_env_com_multiplas_chaves_context7_api() {
1307 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1308 let conteudo_env = "CONTEXT7_API=ctx7sk-cwd-01\nCONTEXT7_API=ctx7sk-cwd-02\n";
1309 std::fs::write(dir_temp.path().join(".env"), conteudo_env)
1310 .expect("Deve escrever .env temporário");
1311
1312 let cwd_original = std::env::current_dir().expect("Deve obter CWD");
1313 std::env::set_current_dir(dir_temp.path()).expect("Deve mudar CWD para temp");
1314
1315 let resultado = ler_env_cwd();
1316
1317 std::env::set_current_dir(&cwd_original).expect("Deve restaurar CWD");
1318
1319 let chaves = resultado.expect("Deve retornar Some com chaves do .env CWD");
1320 assert_eq!(chaves.len(), 2, "Deve ler 2 chaves do .env");
1321 assert_eq!(chaves[0], "ctx7sk-cwd-01");
1322 assert_eq!(chaves[1], "ctx7sk-cwd-02");
1323 }
1324
1325 #[test]
1326 #[serial_test::serial]
1327 fn testa_ler_env_cwd_retorna_none_quando_env_ausente() {
1328 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1329
1330 let cwd_original = std::env::current_dir().expect("Deve obter CWD");
1331 std::env::set_current_dir(dir_temp.path()).expect("Deve mudar CWD para temp sem .env");
1332
1333 let resultado = ler_env_cwd();
1334
1335 std::env::set_current_dir(&cwd_original).expect("Deve restaurar CWD");
1336
1337 assert!(
1338 resultado.is_none(),
1339 "Deve retornar None quando não há .env no CWD"
1340 );
1341 }
1342
1343 #[test]
1344 fn testa_descobrir_caminho_logs_xdg_retorna_algum_caminho_valido() {
1345 let resultado = descobrir_caminho_logs_xdg();
1346
1347 if let Some(caminho) = resultado {
1348 let caminho_str = caminho.to_string_lossy();
1349 assert!(
1350 caminho_str.contains("context7"),
1351 "Caminho de logs XDG deve conter 'context7', obteve: {}",
1352 caminho_str
1353 );
1354 }
1355 }
1356
1357 #[test]
1358 #[serial_test::serial]
1359 fn testa_carregar_chaves_api_env_cwd_usado_quando_env_var_e_xdg_ausentes() {
1360 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1361 let dir_xdg_sem_config = dir_temp.path().join("xdg_sem_config");
1362 std::fs::create_dir_all(&dir_xdg_sem_config).expect("Deve criar diretório XDG vazio");
1363
1364 let dir_cwd = dir_temp.path().join("cwd_com_env");
1365 std::fs::create_dir_all(&dir_cwd).expect("Deve criar CWD temporário");
1366 std::fs::write(dir_cwd.join(".env"), "CONTEXT7_API=ctx7sk-cwd-camada-3\n")
1367 .expect("Deve escrever .env no CWD");
1368
1369 unsafe {
1371 std::env::remove_var("CONTEXT7_API_KEYS");
1372 std::env::set_var("CONTEXT7_HOME", &dir_xdg_sem_config);
1373 }
1374 let cwd_original = std::env::current_dir().expect("Deve obter CWD");
1375 std::env::set_current_dir(&dir_cwd).expect("Deve mudar CWD");
1376
1377 let resultado = carregar_chaves_api();
1378
1379 std::env::set_current_dir(&cwd_original).expect("Deve restaurar CWD");
1380 unsafe {
1381 std::env::remove_var("CONTEXT7_HOME");
1382 }
1383
1384 let chaves = resultado.expect("Deve carregar chaves via .env CWD");
1385 assert_eq!(chaves.len(), 1);
1386 assert_eq!(chaves[0], "ctx7sk-cwd-camada-3");
1387 }
1388
1389 #[test]
1390 #[serial_test::serial]
1391 fn testa_carregar_chaves_api_faz_fallback_quando_xdg_invalido() {
1392 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1393 let dir_context7 = dir_temp.path().join("context7");
1394 std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
1395
1396 std::fs::write(dir_context7.join("config.toml"), "[[[invalido")
1397 .expect("Deve escrever TOML inválido");
1398
1399 let dir_cwd = dir_temp.path().join("cwd_fallback");
1400 std::fs::create_dir_all(&dir_cwd).expect("Deve criar CWD com .env");
1401 std::fs::write(dir_cwd.join(".env"), "CONTEXT7_API=ctx7sk-fallback-cwd\n")
1402 .expect("Deve escrever .env no CWD");
1403
1404 unsafe {
1406 std::env::remove_var("CONTEXT7_API_KEYS");
1407 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1408 }
1409 let cwd_original = std::env::current_dir().expect("Deve obter CWD");
1410 std::env::set_current_dir(&dir_cwd).expect("Deve mudar CWD");
1411
1412 let resultado = carregar_chaves_api();
1413
1414 std::env::set_current_dir(&cwd_original).expect("Deve restaurar CWD");
1415 unsafe {
1416 std::env::remove_var("CONTEXT7_HOME");
1417 }
1418
1419 let chaves = resultado.expect("Deve carregar chaves via fallback .env CWD");
1420 assert_eq!(chaves.len(), 1);
1421 assert_eq!(chaves[0], "ctx7sk-fallback-cwd");
1422 }
1423
1424 #[test]
1427 #[serial_test::serial]
1428 fn testa_cmd_keys_add_cria_config_quando_nao_existe() {
1429 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1430 unsafe {
1432 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1433 }
1434
1435 let resultado = cmd_keys_add("ctx7sk-nova-chave-add-test");
1436
1437 unsafe {
1438 std::env::remove_var("CONTEXT7_HOME");
1439 }
1440
1441 resultado.expect("cmd_keys_add deve funcionar em config vazio");
1442
1443 let caminho = dir_temp.path().join("context7").join("config.toml");
1444 assert!(
1445 caminho.exists(),
1446 "config.toml deve existir após cmd_keys_add"
1447 );
1448
1449 let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config criado");
1450 assert_eq!(config.keys.len(), 1, "Config deve ter 1 chave");
1451 assert_eq!(config.keys[0].value, "ctx7sk-nova-chave-add-test");
1452 }
1453
1454 #[test]
1455 #[serial_test::serial]
1456 fn testa_cmd_keys_add_acumula_em_config_existente() {
1457 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1458 unsafe {
1460 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1461 }
1462
1463 cmd_keys_add("ctx7sk-chave-um").expect("Primeira adição deve funcionar");
1464 cmd_keys_add("ctx7sk-chave-dois").expect("Segunda adição deve funcionar");
1465
1466 let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1467 let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config");
1468
1469 unsafe {
1470 std::env::remove_var("CONTEXT7_HOME");
1471 }
1472
1473 assert_eq!(config.keys.len(), 2, "Deve acumular 2 chaves");
1474 assert_eq!(config.keys[0].value, "ctx7sk-chave-um");
1475 assert_eq!(config.keys[1].value, "ctx7sk-chave-dois");
1476 }
1477
1478 #[test]
1479 #[serial_test::serial]
1480 fn testa_cmd_keys_add_nao_duplica_chave_existente() {
1481 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1482 unsafe {
1484 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1485 }
1486
1487 cmd_keys_add("ctx7sk-unica-dedup").expect("Primeira adição deve funcionar");
1488 cmd_keys_add("ctx7sk-unica-dedup").expect("Segunda adição da mesma chave não deve falhar");
1489
1490 let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1491 let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config");
1492
1493 unsafe {
1494 std::env::remove_var("CONTEXT7_HOME");
1495 }
1496
1497 assert_eq!(config.keys.len(), 1, "Não deve duplicar chave já existente");
1498 }
1499
1500 #[test]
1501 #[cfg(unix)]
1502 #[serial_test::serial]
1503 fn testa_cmd_keys_add_aplica_permissoes_600_em_unix() {
1504 use std::os::unix::fs::PermissionsExt;
1505
1506 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1507 unsafe {
1509 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1510 }
1511
1512 cmd_keys_add("ctx7sk-perm-600-keys-add").expect("Deve adicionar chave sem erro");
1513
1514 let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1515 unsafe {
1516 std::env::remove_var("CONTEXT7_HOME");
1517 }
1518
1519 let metadados = std::fs::metadata(&caminho).expect("Deve obter metadados");
1520 let modo = metadados.permissions().mode() & 0o777;
1521 assert_eq!(
1522 modo, 0o600,
1523 "Permissões devem ser 600 após cmd_keys_add, obteve: {:o}",
1524 modo
1525 );
1526 }
1527
1528 #[test]
1531 #[serial_test::serial]
1532 fn testa_cmd_keys_remove_indice_1_de_config_com_3_chaves() {
1533 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1534 unsafe {
1536 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1537 }
1538
1539 escrever_config_xdg("ctx7sk-rem-alpha").expect("Deve escrever chave 1");
1540 escrever_config_xdg("ctx7sk-rem-beta").expect("Deve escrever chave 2");
1541 escrever_config_xdg("ctx7sk-rem-gamma").expect("Deve escrever chave 3");
1542
1543 cmd_keys_remove(1).expect("Remove índice 1 deve funcionar");
1544
1545 let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1546 let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config");
1547
1548 unsafe {
1549 std::env::remove_var("CONTEXT7_HOME");
1550 }
1551
1552 assert_eq!(config.keys.len(), 2, "Devem sobrar 2 chaves após remoção");
1553 assert_eq!(config.keys[0].value, "ctx7sk-rem-beta");
1554 assert_eq!(config.keys[1].value, "ctx7sk-rem-gamma");
1555 }
1556
1557 #[test]
1558 #[serial_test::serial]
1559 fn testa_cmd_keys_remove_indice_2_de_config_com_3_chaves() {
1560 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1561 unsafe {
1563 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1564 }
1565
1566 escrever_config_xdg("ctx7sk-mid-alpha").expect("Deve escrever chave 1");
1567 escrever_config_xdg("ctx7sk-mid-beta").expect("Deve escrever chave 2");
1568 escrever_config_xdg("ctx7sk-mid-gamma").expect("Deve escrever chave 3");
1569
1570 cmd_keys_remove(2).expect("Remove índice 2 deve funcionar");
1571
1572 let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1573 let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config");
1574
1575 unsafe {
1576 std::env::remove_var("CONTEXT7_HOME");
1577 }
1578
1579 assert_eq!(
1580 config.keys.len(),
1581 2,
1582 "Devem sobrar 2 chaves após remoção da do meio"
1583 );
1584 assert_eq!(config.keys[0].value, "ctx7sk-mid-alpha");
1585 assert_eq!(config.keys[1].value, "ctx7sk-mid-gamma");
1586 }
1587
1588 #[test]
1589 #[serial_test::serial]
1590 fn testa_cmd_keys_remove_indice_zero_retorna_ok_com_mensagem() {
1591 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1592 unsafe {
1594 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1595 }
1596
1597 escrever_config_xdg("ctx7sk-idx-zero-test").expect("Deve escrever chave");
1598
1599 let resultado = cmd_keys_remove(0);
1600
1601 unsafe {
1602 std::env::remove_var("CONTEXT7_HOME");
1603 }
1604
1605 assert!(
1606 resultado.is_ok(),
1607 "Índice 0 inválido deve retornar Ok (não Err), obteve: {:?}",
1608 resultado
1609 );
1610 }
1611
1612 #[test]
1613 #[serial_test::serial]
1614 fn testa_cmd_keys_remove_indice_maior_que_len_retorna_ok_com_mensagem() {
1615 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1616 unsafe {
1618 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1619 }
1620
1621 escrever_config_xdg("ctx7sk-overflow-test").expect("Deve escrever chave");
1622
1623 let resultado = cmd_keys_remove(99);
1624
1625 unsafe {
1626 std::env::remove_var("CONTEXT7_HOME");
1627 }
1628
1629 assert!(
1630 resultado.is_ok(),
1631 "Índice fora do range deve retornar Ok (não Err), obteve: {:?}",
1632 resultado
1633 );
1634 }
1635
1636 #[test]
1637 #[serial_test::serial]
1638 fn testa_cmd_keys_remove_em_config_vazio_retorna_ok() {
1639 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1640 unsafe {
1642 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1643 }
1644
1645 let resultado = cmd_keys_remove(1);
1646
1647 unsafe {
1648 std::env::remove_var("CONTEXT7_HOME");
1649 }
1650
1651 assert!(
1652 resultado.is_ok(),
1653 "Remover de config vazio deve retornar Ok, obteve: {:?}",
1654 resultado
1655 );
1656 }
1657
1658 #[test]
1661 #[serial_test::serial]
1662 fn testa_cmd_keys_clear_com_yes_true_limpa_todas_as_chaves() {
1663 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1664 unsafe {
1666 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1667 }
1668
1669 escrever_config_xdg("ctx7sk-clear-alpha").expect("Deve escrever chave 1");
1670 escrever_config_xdg("ctx7sk-clear-beta").expect("Deve escrever chave 2");
1671
1672 let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1673 let antes = ler_config_toml_do_caminho(&caminho).expect("Deve ler config antes");
1674 assert_eq!(antes.keys.len(), 2, "Pré-condição: 2 chaves antes do clear");
1675
1676 cmd_keys_clear(true).expect("clear com yes=true deve funcionar");
1677
1678 let depois = ler_config_toml_do_caminho(&caminho).expect("Deve ler config depois");
1679
1680 unsafe {
1681 std::env::remove_var("CONTEXT7_HOME");
1682 }
1683
1684 assert!(
1685 depois.keys.is_empty(),
1686 "Após clear com yes=true, chaves devem estar vazias"
1687 );
1688 }
1689
1690 #[test]
1691 #[serial_test::serial]
1692 fn testa_cmd_keys_clear_com_yes_true_em_config_inexistente_funciona() {
1693 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1694 unsafe {
1696 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1697 }
1698
1699 let resultado = cmd_keys_clear(true);
1700
1701 unsafe {
1702 std::env::remove_var("CONTEXT7_HOME");
1703 }
1704
1705 assert!(
1706 resultado.is_ok(),
1707 "clear em config inexistente deve retornar Ok (idempotente), obteve: {:?}",
1708 resultado
1709 );
1710 }
1711
1712 #[test]
1715 #[serial_test::serial]
1716 fn testa_cmd_keys_import_env_valido_com_multiplas_chaves() {
1717 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1718 let arquivo_env = dir_temp.path().join("chaves.env");
1719 std::fs::write(
1720 &arquivo_env,
1721 "CONTEXT7_API=ctx7sk-import-alpha\nCONTEXT7_API=ctx7sk-import-beta\n",
1722 )
1723 .expect("Deve escrever .env de teste");
1724
1725 unsafe {
1727 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1728 }
1729
1730 let resultado = cmd_keys_import(&arquivo_env);
1731
1732 let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1733 let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config após import");
1734
1735 unsafe {
1736 std::env::remove_var("CONTEXT7_HOME");
1737 }
1738
1739 resultado.expect("import de .env válido deve funcionar");
1740 assert_eq!(config.keys.len(), 2, "Deve ter importado 2 chaves");
1741
1742 let valores: Vec<&str> = config.keys.iter().map(|c| c.value.as_str()).collect();
1743 assert!(valores.contains(&"ctx7sk-import-alpha"));
1744 assert!(valores.contains(&"ctx7sk-import-beta"));
1745 }
1746
1747 #[test]
1748 #[serial_test::serial]
1749 fn testa_cmd_keys_import_env_sem_chaves_retorna_err() {
1750 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1751 let arquivo_env = dir_temp.path().join("vazio.env");
1752 std::fs::write(&arquivo_env, "# apenas comentario\nOUTRA_VAR=valor\n")
1753 .expect("Deve escrever .env sem chaves");
1754
1755 unsafe {
1757 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1758 }
1759
1760 let resultado = cmd_keys_import(&arquivo_env);
1761
1762 unsafe {
1763 std::env::remove_var("CONTEXT7_HOME");
1764 }
1765
1766 assert!(
1767 resultado.is_err(),
1768 "Import de .env sem chaves CONTEXT7_API deve retornar Err"
1769 );
1770 }
1771
1772 #[test]
1773 #[serial_test::serial]
1774 fn testa_cmd_keys_import_arquivo_inexistente_retorna_err() {
1775 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1776 let arquivo_inexistente = dir_temp.path().join("nao_existe.env");
1777
1778 unsafe {
1780 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1781 }
1782
1783 let resultado = cmd_keys_import(&arquivo_inexistente);
1784
1785 unsafe {
1786 std::env::remove_var("CONTEXT7_HOME");
1787 }
1788
1789 assert!(
1790 resultado.is_err(),
1791 "Import de arquivo inexistente deve retornar Err"
1792 );
1793 }
1794
1795 #[test]
1796 #[serial_test::serial]
1797 fn testa_cmd_keys_import_roundtrip_add_depois_list() {
1798 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1799 let arquivo_env = dir_temp.path().join("roundtrip.env");
1800 std::fs::write(
1801 &arquivo_env,
1802 "CONTEXT7_API=ctx7sk-rtrip-01\nCONTEXT7_API=ctx7sk-rtrip-02\n",
1803 )
1804 .expect("Deve escrever .env de roundtrip");
1805
1806 unsafe {
1808 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1809 }
1810
1811 cmd_keys_import(&arquivo_env).expect("Import deve funcionar");
1812
1813 let config = ler_config_xdg_raw()
1814 .expect("Deve retornar Ok")
1815 .expect("Deve retornar Some após import");
1816
1817 unsafe {
1818 std::env::remove_var("CONTEXT7_HOME");
1819 }
1820
1821 assert_eq!(
1822 config.keys.len(),
1823 2,
1824 "Roundtrip: deve ter 2 chaves após import"
1825 );
1826 assert_eq!(config.keys[0].value, "ctx7sk-rtrip-01");
1827 assert_eq!(config.keys[1].value, "ctx7sk-rtrip-02");
1828 }
1829
1830 #[test]
1833 #[serial_test::serial]
1834 fn testa_cmd_keys_export_em_config_vazio_retorna_ok() {
1835 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1836 unsafe {
1838 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1839 }
1840
1841 let resultado = cmd_keys_export();
1842
1843 unsafe {
1844 std::env::remove_var("CONTEXT7_HOME");
1845 }
1846
1847 assert!(
1848 resultado.is_ok(),
1849 "Export de config vazio deve retornar Ok, obteve: {:?}",
1850 resultado
1851 );
1852 }
1853
1854 #[test]
1855 #[serial_test::serial]
1856 fn testa_cmd_keys_export_retorna_ok_com_chaves_existentes() {
1857 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1858 unsafe {
1860 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1861 }
1862
1863 escrever_config_xdg("ctx7sk-export-um").expect("Deve escrever chave 1");
1864 escrever_config_xdg("ctx7sk-export-dois").expect("Deve escrever chave 2");
1865
1866 let resultado = cmd_keys_export();
1867
1868 unsafe {
1869 std::env::remove_var("CONTEXT7_HOME");
1870 }
1871
1872 assert!(
1873 resultado.is_ok(),
1874 "Export com chaves existentes deve retornar Ok, obteve: {:?}",
1875 resultado
1876 );
1877 }
1878
1879 #[test]
1882 #[serial_test::serial]
1883 fn testa_ler_config_xdg_raw_retorna_none_sem_arquivo() {
1884 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1885 unsafe {
1887 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1888 }
1889
1890 let resultado = ler_config_xdg_raw();
1891
1892 unsafe {
1893 std::env::remove_var("CONTEXT7_HOME");
1894 }
1895
1896 let valor = resultado.expect("Deve retornar Ok");
1897 assert!(
1898 valor.is_none(),
1899 "Deve retornar None quando config.toml não existe"
1900 );
1901 }
1902
1903 #[test]
1904 #[serial_test::serial]
1905 fn testa_ler_config_xdg_raw_retorna_config_com_chaves() {
1906 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1907 let dir_context7 = dir_temp.path().join("context7");
1908 std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
1909
1910 let toml = r#"schema_version = 1
1911
1912[[keys]]
1913value = "ctx7sk-raw-01"
1914added_at = "2026-04-08T00:00:00+00:00"
1915
1916[[keys]]
1917value = "ctx7sk-raw-02"
1918added_at = "2026-04-08T00:01:00+00:00"
1919"#;
1920 std::fs::write(dir_context7.join("config.toml"), toml).expect("Deve escrever config.toml");
1921
1922 unsafe {
1924 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1925 }
1926
1927 let resultado = ler_config_xdg_raw();
1928
1929 unsafe {
1930 std::env::remove_var("CONTEXT7_HOME");
1931 }
1932
1933 let config = resultado
1934 .expect("Deve retornar Ok")
1935 .expect("Deve retornar Some com config");
1936 assert_eq!(config.keys.len(), 2);
1937 assert_eq!(config.keys[0].value, "ctx7sk-raw-01");
1938 assert_eq!(config.keys[1].value, "ctx7sk-raw-02");
1939 }
1940
1941 #[test]
1942 #[serial_test::serial]
1943 fn testa_cmd_keys_path_retorna_ok() {
1944 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1945 unsafe {
1947 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1948 }
1949
1950 let resultado = cmd_keys_path();
1951
1952 unsafe {
1953 std::env::remove_var("CONTEXT7_HOME");
1954 }
1955
1956 resultado.expect("cmd_keys_path deve retornar Ok");
1957 }
1958
1959 #[test]
1960 #[serial_test::serial]
1961 fn testa_descobrir_caminho_config_termina_com_config_toml() {
1962 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1963 unsafe {
1965 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1966 }
1967
1968 let caminho = descobrir_caminho_config();
1969
1970 unsafe {
1971 std::env::remove_var("CONTEXT7_HOME");
1972 }
1973
1974 let caminho = caminho.expect("Deve retornar caminho XDG válido");
1975 assert!(
1976 caminho.to_string_lossy().ends_with("config.toml"),
1977 "Caminho deve terminar com config.toml, obteve: {}",
1978 caminho.display()
1979 );
1980 assert!(
1981 caminho.to_string_lossy().contains("context7"),
1982 "Caminho deve conter 'context7', obteve: {}",
1983 caminho.display()
1984 );
1985 }
1986
1987 #[test]
1990 #[serial_test::serial]
1991 fn testa_fluxo_completo_add_list_remove_clear() {
1992 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1993 unsafe {
1995 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1996 }
1997
1998 cmd_keys_add("ctx7sk-fluxo-01").expect("Add 1 deve funcionar");
1999 cmd_keys_add("ctx7sk-fluxo-02").expect("Add 2 deve funcionar");
2000 cmd_keys_add("ctx7sk-fluxo-03").expect("Add 3 deve funcionar");
2001
2002 let config_antes = ler_config_xdg_raw()
2003 .expect("Ok")
2004 .expect("Some com 3 chaves");
2005 assert_eq!(config_antes.keys.len(), 3, "Deve ter 3 chaves após 3 adds");
2006
2007 cmd_keys_remove(2).expect("Remove índice 2 deve funcionar");
2008
2009 let config_pos_remove = ler_config_xdg_raw()
2010 .expect("Ok")
2011 .expect("Some com 2 chaves");
2012 assert_eq!(
2013 config_pos_remove.keys.len(),
2014 2,
2015 "Deve ter 2 chaves após remove"
2016 );
2017 assert_eq!(config_pos_remove.keys[0].value, "ctx7sk-fluxo-01");
2018 assert_eq!(config_pos_remove.keys[1].value, "ctx7sk-fluxo-03");
2019
2020 cmd_keys_clear(true).expect("Clear com yes=true deve funcionar");
2021
2022 let caminho = descobrir_caminho_config().expect("Deve ter caminho");
2023 let config_final = ler_config_toml_do_caminho(&caminho).expect("Deve ler config final");
2024
2025 unsafe {
2026 std::env::remove_var("CONTEXT7_HOME");
2027 }
2028
2029 assert!(
2030 config_final.keys.is_empty(),
2031 "Após clear, chaves devem estar vazias"
2032 );
2033 }
2034
2035 #[test]
2038 #[serial_test::serial]
2039 fn testa_context7_home_override_config_path() {
2040 let tmp = tempfile::TempDir::new().expect("Deve criar tempdir");
2041 unsafe {
2044 std::env::set_var("CONTEXT7_HOME", tmp.path());
2045 }
2046
2047 let caminho = descobrir_caminho_config();
2048
2049 unsafe {
2050 std::env::remove_var("CONTEXT7_HOME");
2051 }
2052
2053 let caminho = caminho.expect("Deve retornar Some quando CONTEXT7_HOME está definido");
2054 let esperado = tmp.path().join("context7").join("config.toml");
2055 assert_eq!(
2056 caminho, esperado,
2057 "CONTEXT7_HOME deve definir caminho como {{CONTEXT7_HOME}}/context7/config.toml"
2058 );
2059 }
2060
2061 #[test]
2062 #[serial_test::serial]
2063 fn testa_context7_home_override_logs_path() {
2064 let tmp = tempfile::TempDir::new().expect("Deve criar tempdir");
2065 unsafe {
2067 std::env::set_var("CONTEXT7_HOME", tmp.path());
2068 }
2069
2070 let caminho = descobrir_caminho_logs_xdg();
2071
2072 unsafe {
2073 std::env::remove_var("CONTEXT7_HOME");
2074 }
2075
2076 let caminho = caminho.expect("Deve retornar Some quando CONTEXT7_HOME está definido");
2077 let esperado = tmp.path().join("context7").join("logs");
2078 assert_eq!(
2079 caminho, esperado,
2080 "CONTEXT7_HOME deve definir logs como {{CONTEXT7_HOME}}/context7/logs"
2081 );
2082 }
2083
2084 #[test]
2085 #[serial_test::serial]
2086 fn testa_context7_home_vazio_cai_em_projectdirs() {
2087 let tmp = tempfile::TempDir::new().expect("Deve criar tempdir");
2088 unsafe {
2090 std::env::set_var("CONTEXT7_HOME", "");
2091 }
2092
2093 let caminho = descobrir_caminho_config();
2094
2095 unsafe {
2096 std::env::remove_var("CONTEXT7_HOME");
2097 }
2098
2099 if let Some(c) = caminho {
2101 let tmp_str = tmp.path().to_string_lossy();
2102 assert!(
2103 !c.to_string_lossy().starts_with(tmp_str.as_ref()),
2104 "CONTEXT7_HOME vazio não deve usar o tempdir: {}",
2105 c.display()
2106 );
2107 }
2108 }
2110}