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