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