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" | "y" | "yes"
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")
603 || mensagem_erro.contains("CONTEXT7_API")
604 || mensagem_erro.contains("key")
605 || mensagem_erro.contains("API"),
606 "Mensagem de erro deve mencionar CONTEXT7_API, chave, key ou API, obteve: {}",
607 mensagem_erro
608 );
609 }
610
611 #[test]
612 fn testa_parsing_env_ignora_chaves_vazias() {
613 let conteudo = "CONTEXT7_API=\n\
614 CONTEXT7_API=ctx7sk-valida\n";
615 let chaves = extrair_chaves_env(conteudo).expect("Deve extrair chave sem erro");
616 assert_eq!(
617 chaves.len(),
618 1,
619 "Deve ignorar entradas CONTEXT7_API sem valor"
620 );
621 assert_eq!(chaves[0], "ctx7sk-valida");
622 }
623
624 #[test]
625 fn testa_parsing_env_ignora_comentario_inline() {
626 let conteudo = "CONTEXT7_API=ctx7sk-valida # comentário aqui\n";
627 let chaves = extrair_chaves_env(conteudo).expect("Deve extrair chave sem erro");
628 assert_eq!(chaves.len(), 1);
629 assert_eq!(chaves[0], "ctx7sk-valida");
630 }
631
632 #[test]
635 fn testa_mascarar_chave_com_valor_longo_exibe_prefixo_e_sufixo() {
636 let chave = "ctx7sk-abc123-def456-ghi789";
637 assert_eq!(chave.len(), 27, "Pré-condição: chave deve ter 27 chars");
638 let mascarada = mascarar_chave(chave);
639 assert!(
640 mascarada.starts_with("ctx7sk-abc12"),
641 "Deve iniciar com os primeiros 12 chars, obteve: {}",
642 mascarada
643 );
644 assert!(
645 mascarada.ends_with("i789"),
646 "Deve terminar com os últimos 4 chars, obteve: {}",
647 mascarada
648 );
649 assert!(
650 mascarada.contains("..."),
651 "Deve conter '...' entre prefixo e sufixo, obteve: {}",
652 mascarada
653 );
654 }
655
656 #[test]
657 fn testa_mascarar_chave_curta_retorna_asteriscos() {
658 let chave_exatamente_16 = "ctx7sk-abcdef012";
659 assert_eq!(
660 chave_exatamente_16.len(),
661 16,
662 "Pré-condição: chave deve ter 16 chars"
663 );
664 let mascarada = mascarar_chave(chave_exatamente_16);
665 assert_eq!(
666 mascarada, "***",
667 "Chave de 16 chars deve retornar '***', obteve: {}",
668 mascarada
669 );
670 }
671
672 #[test]
673 fn testa_mascarar_chave_vazia_retorna_asteriscos() {
674 let mascarada = mascarar_chave("");
675 assert_eq!(
676 mascarada, "***",
677 "Chave vazia deve retornar '***', obteve: {}",
678 mascarada
679 );
680 }
681
682 #[test]
683 fn testa_mascarar_chave_de_exatamente_17_chars_mascara_corretamente() {
684 let chave = "ctx7sk-abcdef0123"; assert_eq!(chave.len(), 17, "Pré-condição: chave deve ter 17 chars");
686 let mascarada = mascarar_chave(chave);
687 assert!(
688 mascarada.contains("..."),
689 "Chave de 17 chars deve ser mascarada, obteve: {}",
690 mascarada
691 );
692 assert_eq!(
693 &mascarada[..12],
694 &chave[..12],
695 "Prefixo de 12 chars deve ser preservado"
696 );
697 assert!(
698 mascarada.ends_with(&chave[chave.len() - 4..]),
699 "Sufixo de 4 chars deve ser preservado"
700 );
701 }
702
703 #[test]
706 #[serial_test::serial]
707 fn testa_ler_env_var_chave_retorna_some_quando_setada() {
708 unsafe {
711 std::env::set_var("CONTEXT7_API_KEYS", "ctx7sk-chave-teste-01");
712 }
713 let resultado = ler_env_var_chave();
714 unsafe {
715 std::env::remove_var("CONTEXT7_API_KEYS");
716 }
717
718 let chaves = resultado.expect("Deve retornar Some com chave válida");
719 assert_eq!(chaves.len(), 1, "Deve retornar exatamente 1 chave");
720 assert_eq!(chaves[0], "ctx7sk-chave-teste-01");
721 }
722
723 #[test]
724 #[serial_test::serial]
725 fn testa_ler_env_var_chave_aceita_multiplas_separadas_por_virgula() {
726 unsafe {
728 std::env::set_var(
729 "CONTEXT7_API_KEYS",
730 "ctx7sk-chave-a, ctx7sk-chave-b , ctx7sk-chave-c",
731 );
732 }
733 let resultado = ler_env_var_chave();
734 unsafe {
735 std::env::remove_var("CONTEXT7_API_KEYS");
736 }
737
738 let chaves = resultado.expect("Deve retornar Some com múltiplas chaves");
739 assert_eq!(chaves.len(), 3, "Deve retornar 3 chaves");
740 assert_eq!(chaves[0], "ctx7sk-chave-a");
741 assert_eq!(chaves[1], "ctx7sk-chave-b");
742 assert_eq!(chaves[2], "ctx7sk-chave-c");
743 }
744
745 #[test]
746 #[serial_test::serial]
747 fn testa_ler_env_var_chave_retorna_none_quando_vazia() {
748 unsafe {
750 std::env::set_var("CONTEXT7_API_KEYS", "");
751 }
752 let resultado = ler_env_var_chave();
753 unsafe {
754 std::env::remove_var("CONTEXT7_API_KEYS");
755 }
756
757 assert!(
758 resultado.is_none(),
759 "Deve retornar None quando env var está vazia"
760 );
761 }
762
763 #[test]
764 #[serial_test::serial]
765 fn testa_ler_env_var_chave_retorna_none_quando_apenas_whitespace() {
766 unsafe {
768 std::env::set_var("CONTEXT7_API_KEYS", " , , ");
769 }
770 let resultado = ler_env_var_chave();
771 unsafe {
772 std::env::remove_var("CONTEXT7_API_KEYS");
773 }
774
775 assert!(
776 resultado.is_none(),
777 "Deve retornar None quando env var contém apenas whitespace/vírgulas"
778 );
779 }
780
781 #[test]
782 #[serial_test::serial]
783 fn testa_ler_env_var_chave_retorna_none_quando_ausente() {
784 unsafe {
786 std::env::remove_var("CONTEXT7_API_KEYS");
787 }
788 let resultado = ler_env_var_chave();
789
790 assert!(
791 resultado.is_none(),
792 "Deve retornar None quando env var não existe"
793 );
794 }
795
796 #[test]
799 #[serial_test::serial]
800 fn testa_ler_config_xdg_arquivo_inexistente_retorna_none() {
801 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
802 unsafe {
804 std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
805 }
806 let resultado = ler_config_xdg();
807 unsafe {
808 std::env::remove_var("XDG_CONFIG_HOME");
809 }
810
811 let valor = resultado.expect("Deve retornar Ok quando arquivo não existe");
812 assert!(
813 valor.is_none(),
814 "Deve retornar None quando config.toml não existe"
815 );
816 }
817
818 #[test]
819 #[serial_test::serial]
820 fn testa_ler_config_xdg_le_toml_valido_com_multiplas_chaves() {
821 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
822 let dir_context7 = dir_temp.path().join("context7");
823 std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
824
825 let toml_conteudo = r#"schema_version = 1
826
827[[keys]]
828value = "ctx7sk-chave-xdg-01"
829added_at = "2026-01-01T00:00:00+00:00"
830
831[[keys]]
832value = "ctx7sk-chave-xdg-02"
833added_at = "2026-01-02T00:00:00+00:00"
834"#;
835 std::fs::write(dir_context7.join("config.toml"), toml_conteudo)
836 .expect("Deve escrever config.toml");
837
838 unsafe {
840 std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
841 }
842 let resultado = ler_config_xdg();
843 unsafe {
844 std::env::remove_var("XDG_CONFIG_HOME");
845 }
846
847 let chaves = resultado
848 .expect("Deve retornar Ok")
849 .expect("Deve retornar Some com chaves");
850 assert_eq!(chaves.len(), 2, "Deve retornar 2 chaves");
851 assert_eq!(chaves[0], "ctx7sk-chave-xdg-01");
852 assert_eq!(chaves[1], "ctx7sk-chave-xdg-02");
853 }
854
855 #[test]
856 #[serial_test::serial]
857 fn testa_ler_config_xdg_retorna_err_quando_toml_invalido() {
858 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
859 let dir_context7 = dir_temp.path().join("context7");
860 std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
861
862 std::fs::write(
863 dir_context7.join("config.toml"),
864 "schema_version = INVALIDO\n[[[malformado",
865 )
866 .expect("Deve escrever TOML inválido");
867
868 unsafe {
870 std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
871 }
872 let resultado = ler_config_xdg();
873 unsafe {
874 std::env::remove_var("XDG_CONFIG_HOME");
875 }
876
877 assert!(
878 resultado.is_err(),
879 "Deve retornar Err quando TOML está malformado"
880 );
881 }
882
883 #[test]
884 #[serial_test::serial]
885 fn testa_ler_config_xdg_preserva_ordem_das_chaves() {
886 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
887 let dir_context7 = dir_temp.path().join("context7");
888 std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
889
890 let toml_conteudo = r#"schema_version = 1
891
892[[keys]]
893value = "ctx7sk-primeira"
894added_at = "2026-01-01T00:00:00+00:00"
895
896[[keys]]
897value = "ctx7sk-segunda"
898added_at = "2026-01-02T00:00:00+00:00"
899
900[[keys]]
901value = "ctx7sk-terceira"
902added_at = "2026-01-03T00: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("XDG_CONFIG_HOME", dir_temp.path());
910 }
911 let resultado = ler_config_xdg();
912 unsafe {
913 std::env::remove_var("XDG_CONFIG_HOME");
914 }
915
916 let chaves = resultado
917 .expect("Deve retornar Ok")
918 .expect("Deve retornar Some");
919 assert_eq!(chaves[0], "ctx7sk-primeira");
920 assert_eq!(chaves[1], "ctx7sk-segunda");
921 assert_eq!(chaves[2], "ctx7sk-terceira");
922 }
923
924 #[test]
925 #[serial_test::serial]
926 fn testa_ler_config_xdg_keys_vazio_retorna_none() {
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 let toml_sem_chaves = "schema_version = 1\n";
932 std::fs::write(dir_context7.join("config.toml"), toml_sem_chaves)
933 .expect("Deve escrever config.toml sem keys");
934
935 unsafe {
937 std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
938 }
939 let resultado = ler_config_xdg();
940 unsafe {
941 std::env::remove_var("XDG_CONFIG_HOME");
942 }
943
944 let valor = resultado.expect("Deve retornar Ok");
945 assert!(
946 valor.is_none(),
947 "Deve retornar None quando config.toml existe mas keys está vazio"
948 );
949 }
950
951 #[test]
954 #[serial_test::serial]
955 fn testa_escrever_config_xdg_roundtrip_serializa_e_deserializa() {
956 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
957 unsafe {
959 std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
960 }
961
962 let caminho =
963 escrever_config_xdg("ctx7sk-roundtrip-01").expect("Deve escrever config sem erro");
964
965 let config_lido = ler_config_toml_do_caminho(&caminho)
966 .expect("Deve ler TOML escrito por escrever_config_xdg");
967
968 unsafe {
969 std::env::remove_var("XDG_CONFIG_HOME");
970 }
971
972 assert_eq!(config_lido.schema_version, 1, "schema_version deve ser 1");
973 assert_eq!(config_lido.keys.len(), 1, "Deve conter 1 chave");
974 assert_eq!(
975 config_lido.keys[0].value, "ctx7sk-roundtrip-01",
976 "Valor da chave deve ser preservado"
977 );
978 assert!(
979 !config_lido.keys[0].added_at.is_empty(),
980 "added_at não deve ser vazio"
981 );
982 }
983
984 #[test]
985 #[serial_test::serial]
986 fn testa_escrever_config_xdg_cria_diretorios_pai_se_nao_existirem() {
987 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
988 let xdg_novo = dir_temp.path().join("xdg_inexistente");
989 unsafe {
991 std::env::set_var("XDG_CONFIG_HOME", &xdg_novo);
992 }
993
994 let resultado = escrever_config_xdg("ctx7sk-mkdir-teste");
995 unsafe {
996 std::env::remove_var("XDG_CONFIG_HOME");
997 }
998
999 let caminho = resultado.expect("Deve criar diretório pai e escrever config");
1000 assert!(
1001 caminho.exists(),
1002 "Arquivo de config deve existir após escrita"
1003 );
1004 }
1005
1006 #[test]
1007 #[serial_test::serial]
1008 fn testa_escrever_config_xdg_nao_duplica_chave_ja_existente() {
1009 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1010 unsafe {
1012 std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1013 }
1014
1015 escrever_config_xdg("ctx7sk-unica").expect("Primeira escrita deve funcionar");
1016 escrever_config_xdg("ctx7sk-unica").expect("Segunda escrita não deve falhar");
1017
1018 let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1019 let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config");
1020
1021 unsafe {
1022 std::env::remove_var("XDG_CONFIG_HOME");
1023 }
1024
1025 assert_eq!(
1026 config.keys.len(),
1027 1,
1028 "Não deve duplicar chave já existente — deve ter apenas 1"
1029 );
1030 }
1031
1032 #[test]
1033 #[serial_test::serial]
1034 fn testa_escrever_config_xdg_acumula_chaves_distintas() {
1035 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1036 unsafe {
1038 std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1039 }
1040
1041 escrever_config_xdg("ctx7sk-chave-a").expect("Primeira escrita deve funcionar");
1042 escrever_config_xdg("ctx7sk-chave-b").expect("Segunda escrita deve funcionar");
1043 escrever_config_xdg("ctx7sk-chave-c").expect("Terceira escrita deve funcionar");
1044
1045 let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1046 let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config");
1047
1048 unsafe {
1049 std::env::remove_var("XDG_CONFIG_HOME");
1050 }
1051
1052 assert_eq!(config.keys.len(), 3, "Deve acumular 3 chaves distintas");
1053 let valores: Vec<&str> = config.keys.iter().map(|c| c.value.as_str()).collect();
1054 assert!(valores.contains(&"ctx7sk-chave-a"));
1055 assert!(valores.contains(&"ctx7sk-chave-b"));
1056 assert!(valores.contains(&"ctx7sk-chave-c"));
1057 }
1058
1059 #[test]
1060 #[cfg(unix)]
1061 #[serial_test::serial]
1062 fn testa_escrever_config_xdg_aplica_permissoes_600_em_unix() {
1063 use std::os::unix::fs::PermissionsExt;
1064
1065 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1066 unsafe {
1068 std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1069 }
1070
1071 let caminho =
1072 escrever_config_xdg("ctx7sk-perm-600").expect("Deve escrever config sem erro");
1073 unsafe {
1074 std::env::remove_var("XDG_CONFIG_HOME");
1075 }
1076
1077 let metadados = std::fs::metadata(&caminho).expect("Deve obter metadados do arquivo");
1078 let modo = metadados.permissions().mode() & 0o777;
1079
1080 assert_eq!(modo, 0o600, "Permissões devem ser 600, obteve: {:o}", modo);
1081 }
1082
1083 #[test]
1086 fn testa_config_arquivo_roundtrip_serde_preserva_todos_campos() {
1087 let config_original = ConfigArquivo {
1088 schema_version: 1,
1089 keys: vec![
1090 ChaveArmazenada {
1091 value: "ctx7sk-serde-01".to_string(),
1092 added_at: "2026-01-01T12:00:00+00:00".to_string(),
1093 },
1094 ChaveArmazenada {
1095 value: "ctx7sk-serde-02".to_string(),
1096 added_at: "2026-01-02T12:00:00+00:00".to_string(),
1097 },
1098 ],
1099 };
1100
1101 let toml_str = toml::to_string_pretty(&config_original)
1102 .expect("Deve serializar ConfigArquivo para TOML");
1103 let config_deserializado: ConfigArquivo =
1104 toml::from_str(&toml_str).expect("Deve deserializar TOML de volta para ConfigArquivo");
1105
1106 assert_eq!(
1107 config_deserializado.schema_version, config_original.schema_version,
1108 "schema_version deve ser preservado no roundtrip"
1109 );
1110 assert_eq!(
1111 config_deserializado.keys.len(),
1112 config_original.keys.len(),
1113 "Número de chaves deve ser preservado"
1114 );
1115 assert_eq!(
1116 config_deserializado.keys[0].value, config_original.keys[0].value,
1117 "Valor da primeira chave deve ser preservado"
1118 );
1119 assert_eq!(
1120 config_deserializado.keys[0].added_at, config_original.keys[0].added_at,
1121 "added_at da primeira chave deve ser preservado"
1122 );
1123 }
1124
1125 #[test]
1126 fn testa_config_arquivo_schema_version_sempre_presente_na_serializacao() {
1127 let config = ConfigArquivo {
1128 schema_version: 1,
1129 keys: Vec::new(),
1130 };
1131
1132 let toml_str = toml::to_string_pretty(&config).expect("Deve serializar para TOML");
1133
1134 assert!(
1135 toml_str.contains("schema_version"),
1136 "schema_version deve estar presente na serialização TOML"
1137 );
1138 assert!(toml_str.contains('1'), "Valor 1 deve estar presente");
1139 }
1140
1141 #[test]
1142 fn testa_config_arquivo_keys_vazio_aceito_na_deserializacao() {
1143 let toml_str = "schema_version = 1\n";
1144 let config: ConfigArquivo =
1145 toml::from_str(toml_str).expect("Deve deserializar com keys ausente (default vazio)");
1146
1147 assert_eq!(config.schema_version, 1);
1148 assert!(
1149 config.keys.is_empty(),
1150 "keys deve ser vazio quando não presente no TOML"
1151 );
1152 }
1153
1154 #[test]
1155 fn testa_chave_armazenada_preserva_added_at_como_string_utc() {
1156 let timestamp = "2026-04-08T20:00:00+00:00";
1157 let chave = ChaveArmazenada {
1158 value: "ctx7sk-timestamp".to_string(),
1159 added_at: timestamp.to_string(),
1160 };
1161
1162 let toml_str = toml::to_string_pretty(&chave).expect("Deve serializar ChaveArmazenada");
1163 let chave_de_volta: ChaveArmazenada =
1164 toml::from_str(&toml_str).expect("Deve deserializar ChaveArmazenada");
1165
1166 assert_eq!(
1167 chave_de_volta.added_at, timestamp,
1168 "Timestamp added_at deve ser preservado exatamente"
1169 );
1170 }
1171
1172 #[test]
1175 #[serial_test::serial]
1176 fn testa_carregar_chaves_api_env_var_tem_prioridade_sobre_xdg() {
1177 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1178 let dir_context7 = dir_temp.path().join("context7");
1179 std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
1180
1181 let toml_xdg = r#"schema_version = 1
1182[[keys]]
1183value = "ctx7sk-xdg-deve-ser-ignorada"
1184added_at = "2026-01-01T00:00:00+00:00"
1185"#;
1186 std::fs::write(dir_context7.join("config.toml"), toml_xdg)
1187 .expect("Deve escrever config XDG");
1188
1189 unsafe {
1191 std::env::set_var("CONTEXT7_API_KEYS", "ctx7sk-env-var-prioritaria");
1192 std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1193 }
1194
1195 let resultado = carregar_chaves_api();
1196
1197 unsafe {
1198 std::env::remove_var("CONTEXT7_API_KEYS");
1199 std::env::remove_var("XDG_CONFIG_HOME");
1200 }
1201
1202 let chaves = resultado.expect("Deve carregar chaves via env var");
1203 assert_eq!(chaves.len(), 1);
1204 assert_eq!(
1205 chaves[0], "ctx7sk-env-var-prioritaria",
1206 "Env var deve ter prioridade sobre XDG"
1207 );
1208 }
1209
1210 #[test]
1211 #[serial_test::serial]
1212 fn testa_carregar_chaves_api_xdg_usado_quando_env_var_ausente() {
1213 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1214 let dir_context7 = dir_temp.path().join("context7");
1215 std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
1216
1217 let toml_xdg = r#"schema_version = 1
1218[[keys]]
1219value = "ctx7sk-via-xdg"
1220added_at = "2026-01-01T00:00:00+00:00"
1221"#;
1222 std::fs::write(dir_context7.join("config.toml"), toml_xdg)
1223 .expect("Deve escrever config XDG");
1224
1225 unsafe {
1227 std::env::remove_var("CONTEXT7_API_KEYS");
1228 std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1229 }
1230
1231 let resultado = carregar_chaves_api();
1232
1233 unsafe {
1234 std::env::remove_var("XDG_CONFIG_HOME");
1235 }
1236
1237 let chaves = resultado.expect("Deve carregar chaves via XDG");
1238 assert_eq!(chaves.len(), 1);
1239 assert_eq!(chaves[0], "ctx7sk-via-xdg");
1240 }
1241
1242 #[test]
1243 #[serial_test::serial]
1244 fn testa_carregar_chaves_api_retorna_err_quando_nada_disponivel() {
1245 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1246 let dir_xdg_vazio = dir_temp.path().join("xdg_vazio");
1247 std::fs::create_dir_all(&dir_xdg_vazio).expect("Deve criar diretório XDG vazio");
1248
1249 let dir_sem_env = dir_temp.path().join("sem_env");
1250 std::fs::create_dir_all(&dir_sem_env).expect("Deve criar diretório sem .env");
1251
1252 unsafe {
1254 std::env::remove_var("CONTEXT7_API_KEYS");
1255 std::env::set_var("XDG_CONFIG_HOME", &dir_xdg_vazio);
1256 }
1257 let cwd_original = std::env::current_dir().expect("Deve obter CWD atual");
1258 std::env::set_current_dir(&dir_sem_env).expect("Deve mudar CWD");
1259
1260 let resultado = carregar_chaves_api();
1261
1262 std::env::set_current_dir(&cwd_original).expect("Deve restaurar CWD");
1263 unsafe {
1264 std::env::remove_var("XDG_CONFIG_HOME");
1265 }
1266
1267 assert!(
1268 resultado.is_err(),
1269 "Deve retornar Err quando nenhuma camada fornecer chaves"
1270 );
1271 }
1272
1273 #[test]
1274 #[serial_test::serial]
1275 fn testa_ler_env_cwd_le_env_com_multiplas_chaves_context7_api() {
1276 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1277 let conteudo_env = "CONTEXT7_API=ctx7sk-cwd-01\nCONTEXT7_API=ctx7sk-cwd-02\n";
1278 std::fs::write(dir_temp.path().join(".env"), conteudo_env)
1279 .expect("Deve escrever .env temporário");
1280
1281 let cwd_original = std::env::current_dir().expect("Deve obter CWD");
1282 std::env::set_current_dir(dir_temp.path()).expect("Deve mudar CWD para temp");
1283
1284 let resultado = ler_env_cwd();
1285
1286 std::env::set_current_dir(&cwd_original).expect("Deve restaurar CWD");
1287
1288 let chaves = resultado.expect("Deve retornar Some com chaves do .env CWD");
1289 assert_eq!(chaves.len(), 2, "Deve ler 2 chaves do .env");
1290 assert_eq!(chaves[0], "ctx7sk-cwd-01");
1291 assert_eq!(chaves[1], "ctx7sk-cwd-02");
1292 }
1293
1294 #[test]
1295 #[serial_test::serial]
1296 fn testa_ler_env_cwd_retorna_none_quando_env_ausente() {
1297 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1298
1299 let cwd_original = std::env::current_dir().expect("Deve obter CWD");
1300 std::env::set_current_dir(dir_temp.path()).expect("Deve mudar CWD para temp sem .env");
1301
1302 let resultado = ler_env_cwd();
1303
1304 std::env::set_current_dir(&cwd_original).expect("Deve restaurar CWD");
1305
1306 assert!(
1307 resultado.is_none(),
1308 "Deve retornar None quando não há .env no CWD"
1309 );
1310 }
1311
1312 #[test]
1313 fn testa_descobrir_caminho_logs_xdg_retorna_algum_caminho_valido() {
1314 let resultado = descobrir_caminho_logs_xdg();
1315
1316 if let Some(caminho) = resultado {
1317 let caminho_str = caminho.to_string_lossy();
1318 assert!(
1319 caminho_str.contains("context7"),
1320 "Caminho de logs XDG deve conter 'context7', obteve: {}",
1321 caminho_str
1322 );
1323 }
1324 }
1325
1326 #[test]
1327 #[serial_test::serial]
1328 fn testa_carregar_chaves_api_env_cwd_usado_quando_env_var_e_xdg_ausentes() {
1329 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1330 let dir_xdg_sem_config = dir_temp.path().join("xdg_sem_config");
1331 std::fs::create_dir_all(&dir_xdg_sem_config).expect("Deve criar diretório XDG vazio");
1332
1333 let dir_cwd = dir_temp.path().join("cwd_com_env");
1334 std::fs::create_dir_all(&dir_cwd).expect("Deve criar CWD temporário");
1335 std::fs::write(dir_cwd.join(".env"), "CONTEXT7_API=ctx7sk-cwd-camada-3\n")
1336 .expect("Deve escrever .env no CWD");
1337
1338 unsafe {
1340 std::env::remove_var("CONTEXT7_API_KEYS");
1341 std::env::set_var("XDG_CONFIG_HOME", &dir_xdg_sem_config);
1342 }
1343 let cwd_original = std::env::current_dir().expect("Deve obter CWD");
1344 std::env::set_current_dir(&dir_cwd).expect("Deve mudar CWD");
1345
1346 let resultado = carregar_chaves_api();
1347
1348 std::env::set_current_dir(&cwd_original).expect("Deve restaurar CWD");
1349 unsafe {
1350 std::env::remove_var("XDG_CONFIG_HOME");
1351 }
1352
1353 let chaves = resultado.expect("Deve carregar chaves via .env CWD");
1354 assert_eq!(chaves.len(), 1);
1355 assert_eq!(chaves[0], "ctx7sk-cwd-camada-3");
1356 }
1357
1358 #[test]
1359 #[serial_test::serial]
1360 fn testa_carregar_chaves_api_faz_fallback_quando_xdg_invalido() {
1361 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1362 let dir_context7 = dir_temp.path().join("context7");
1363 std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
1364
1365 std::fs::write(dir_context7.join("config.toml"), "[[[invalido")
1366 .expect("Deve escrever TOML inválido");
1367
1368 let dir_cwd = dir_temp.path().join("cwd_fallback");
1369 std::fs::create_dir_all(&dir_cwd).expect("Deve criar CWD com .env");
1370 std::fs::write(dir_cwd.join(".env"), "CONTEXT7_API=ctx7sk-fallback-cwd\n")
1371 .expect("Deve escrever .env no CWD");
1372
1373 unsafe {
1375 std::env::remove_var("CONTEXT7_API_KEYS");
1376 std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1377 }
1378 let cwd_original = std::env::current_dir().expect("Deve obter CWD");
1379 std::env::set_current_dir(&dir_cwd).expect("Deve mudar CWD");
1380
1381 let resultado = carregar_chaves_api();
1382
1383 std::env::set_current_dir(&cwd_original).expect("Deve restaurar CWD");
1384 unsafe {
1385 std::env::remove_var("XDG_CONFIG_HOME");
1386 }
1387
1388 let chaves = resultado.expect("Deve carregar chaves via fallback .env CWD");
1389 assert_eq!(chaves.len(), 1);
1390 assert_eq!(chaves[0], "ctx7sk-fallback-cwd");
1391 }
1392
1393 #[test]
1396 #[serial_test::serial]
1397 fn testa_cmd_keys_add_cria_config_quando_nao_existe() {
1398 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1399 unsafe {
1401 std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1402 }
1403
1404 let resultado = cmd_keys_add("ctx7sk-nova-chave-add-test");
1405
1406 unsafe {
1407 std::env::remove_var("XDG_CONFIG_HOME");
1408 }
1409
1410 resultado.expect("cmd_keys_add deve funcionar em config vazio");
1411
1412 let caminho = dir_temp.path().join("context7").join("config.toml");
1413 assert!(
1414 caminho.exists(),
1415 "config.toml deve existir após cmd_keys_add"
1416 );
1417
1418 let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config criado");
1419 assert_eq!(config.keys.len(), 1, "Config deve ter 1 chave");
1420 assert_eq!(config.keys[0].value, "ctx7sk-nova-chave-add-test");
1421 }
1422
1423 #[test]
1424 #[serial_test::serial]
1425 fn testa_cmd_keys_add_acumula_em_config_existente() {
1426 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1427 unsafe {
1429 std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1430 }
1431
1432 cmd_keys_add("ctx7sk-chave-um").expect("Primeira adição deve funcionar");
1433 cmd_keys_add("ctx7sk-chave-dois").expect("Segunda adição deve funcionar");
1434
1435 let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1436 let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config");
1437
1438 unsafe {
1439 std::env::remove_var("XDG_CONFIG_HOME");
1440 }
1441
1442 assert_eq!(config.keys.len(), 2, "Deve acumular 2 chaves");
1443 assert_eq!(config.keys[0].value, "ctx7sk-chave-um");
1444 assert_eq!(config.keys[1].value, "ctx7sk-chave-dois");
1445 }
1446
1447 #[test]
1448 #[serial_test::serial]
1449 fn testa_cmd_keys_add_nao_duplica_chave_existente() {
1450 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1451 unsafe {
1453 std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1454 }
1455
1456 cmd_keys_add("ctx7sk-unica-dedup").expect("Primeira adição deve funcionar");
1457 cmd_keys_add("ctx7sk-unica-dedup").expect("Segunda adição da mesma chave não deve falhar");
1458
1459 let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1460 let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config");
1461
1462 unsafe {
1463 std::env::remove_var("XDG_CONFIG_HOME");
1464 }
1465
1466 assert_eq!(config.keys.len(), 1, "Não deve duplicar chave já existente");
1467 }
1468
1469 #[test]
1470 #[cfg(unix)]
1471 #[serial_test::serial]
1472 fn testa_cmd_keys_add_aplica_permissoes_600_em_unix() {
1473 use std::os::unix::fs::PermissionsExt;
1474
1475 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1476 unsafe {
1478 std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1479 }
1480
1481 cmd_keys_add("ctx7sk-perm-600-keys-add").expect("Deve adicionar chave sem erro");
1482
1483 let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1484 unsafe {
1485 std::env::remove_var("XDG_CONFIG_HOME");
1486 }
1487
1488 let metadados = std::fs::metadata(&caminho).expect("Deve obter metadados");
1489 let modo = metadados.permissions().mode() & 0o777;
1490 assert_eq!(
1491 modo, 0o600,
1492 "Permissões devem ser 600 após cmd_keys_add, obteve: {:o}",
1493 modo
1494 );
1495 }
1496
1497 #[test]
1500 #[serial_test::serial]
1501 fn testa_cmd_keys_remove_indice_1_de_config_com_3_chaves() {
1502 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1503 unsafe {
1505 std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1506 }
1507
1508 escrever_config_xdg("ctx7sk-rem-alpha").expect("Deve escrever chave 1");
1509 escrever_config_xdg("ctx7sk-rem-beta").expect("Deve escrever chave 2");
1510 escrever_config_xdg("ctx7sk-rem-gamma").expect("Deve escrever chave 3");
1511
1512 cmd_keys_remove(1).expect("Remove índice 1 deve funcionar");
1513
1514 let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1515 let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config");
1516
1517 unsafe {
1518 std::env::remove_var("XDG_CONFIG_HOME");
1519 }
1520
1521 assert_eq!(config.keys.len(), 2, "Devem sobrar 2 chaves após remoção");
1522 assert_eq!(config.keys[0].value, "ctx7sk-rem-beta");
1523 assert_eq!(config.keys[1].value, "ctx7sk-rem-gamma");
1524 }
1525
1526 #[test]
1527 #[serial_test::serial]
1528 fn testa_cmd_keys_remove_indice_2_de_config_com_3_chaves() {
1529 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1530 unsafe {
1532 std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1533 }
1534
1535 escrever_config_xdg("ctx7sk-mid-alpha").expect("Deve escrever chave 1");
1536 escrever_config_xdg("ctx7sk-mid-beta").expect("Deve escrever chave 2");
1537 escrever_config_xdg("ctx7sk-mid-gamma").expect("Deve escrever chave 3");
1538
1539 cmd_keys_remove(2).expect("Remove índice 2 deve funcionar");
1540
1541 let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1542 let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config");
1543
1544 unsafe {
1545 std::env::remove_var("XDG_CONFIG_HOME");
1546 }
1547
1548 assert_eq!(
1549 config.keys.len(),
1550 2,
1551 "Devem sobrar 2 chaves após remoção da do meio"
1552 );
1553 assert_eq!(config.keys[0].value, "ctx7sk-mid-alpha");
1554 assert_eq!(config.keys[1].value, "ctx7sk-mid-gamma");
1555 }
1556
1557 #[test]
1558 #[serial_test::serial]
1559 fn testa_cmd_keys_remove_indice_zero_retorna_ok_com_mensagem() {
1560 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1561 unsafe {
1563 std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1564 }
1565
1566 escrever_config_xdg("ctx7sk-idx-zero-test").expect("Deve escrever chave");
1567
1568 let resultado = cmd_keys_remove(0);
1569
1570 unsafe {
1571 std::env::remove_var("XDG_CONFIG_HOME");
1572 }
1573
1574 assert!(
1575 resultado.is_ok(),
1576 "Índice 0 inválido deve retornar Ok (não Err), obteve: {:?}",
1577 resultado
1578 );
1579 }
1580
1581 #[test]
1582 #[serial_test::serial]
1583 fn testa_cmd_keys_remove_indice_maior_que_len_retorna_ok_com_mensagem() {
1584 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1585 unsafe {
1587 std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1588 }
1589
1590 escrever_config_xdg("ctx7sk-overflow-test").expect("Deve escrever chave");
1591
1592 let resultado = cmd_keys_remove(99);
1593
1594 unsafe {
1595 std::env::remove_var("XDG_CONFIG_HOME");
1596 }
1597
1598 assert!(
1599 resultado.is_ok(),
1600 "Índice fora do range deve retornar Ok (não Err), obteve: {:?}",
1601 resultado
1602 );
1603 }
1604
1605 #[test]
1606 #[serial_test::serial]
1607 fn testa_cmd_keys_remove_em_config_vazio_retorna_ok() {
1608 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1609 unsafe {
1611 std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1612 }
1613
1614 let resultado = cmd_keys_remove(1);
1615
1616 unsafe {
1617 std::env::remove_var("XDG_CONFIG_HOME");
1618 }
1619
1620 assert!(
1621 resultado.is_ok(),
1622 "Remover de config vazio deve retornar Ok, obteve: {:?}",
1623 resultado
1624 );
1625 }
1626
1627 #[test]
1630 #[serial_test::serial]
1631 fn testa_cmd_keys_clear_com_yes_true_limpa_todas_as_chaves() {
1632 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1633 unsafe {
1635 std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1636 }
1637
1638 escrever_config_xdg("ctx7sk-clear-alpha").expect("Deve escrever chave 1");
1639 escrever_config_xdg("ctx7sk-clear-beta").expect("Deve escrever chave 2");
1640
1641 let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1642 let antes = ler_config_toml_do_caminho(&caminho).expect("Deve ler config antes");
1643 assert_eq!(antes.keys.len(), 2, "Pré-condição: 2 chaves antes do clear");
1644
1645 cmd_keys_clear(true).expect("clear com yes=true deve funcionar");
1646
1647 let depois = ler_config_toml_do_caminho(&caminho).expect("Deve ler config depois");
1648
1649 unsafe {
1650 std::env::remove_var("XDG_CONFIG_HOME");
1651 }
1652
1653 assert!(
1654 depois.keys.is_empty(),
1655 "Após clear com yes=true, chaves devem estar vazias"
1656 );
1657 }
1658
1659 #[test]
1660 #[serial_test::serial]
1661 fn testa_cmd_keys_clear_com_yes_true_em_config_inexistente_funciona() {
1662 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1663 unsafe {
1665 std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1666 }
1667
1668 let resultado = cmd_keys_clear(true);
1669
1670 unsafe {
1671 std::env::remove_var("XDG_CONFIG_HOME");
1672 }
1673
1674 assert!(
1675 resultado.is_ok(),
1676 "clear em config inexistente deve retornar Ok (idempotente), obteve: {:?}",
1677 resultado
1678 );
1679 }
1680
1681 #[test]
1684 #[serial_test::serial]
1685 fn testa_cmd_keys_import_env_valido_com_multiplas_chaves() {
1686 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1687 let arquivo_env = dir_temp.path().join("chaves.env");
1688 std::fs::write(
1689 &arquivo_env,
1690 "CONTEXT7_API=ctx7sk-import-alpha\nCONTEXT7_API=ctx7sk-import-beta\n",
1691 )
1692 .expect("Deve escrever .env de teste");
1693
1694 unsafe {
1696 std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1697 }
1698
1699 let resultado = cmd_keys_import(&arquivo_env);
1700
1701 let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1702 let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config após import");
1703
1704 unsafe {
1705 std::env::remove_var("XDG_CONFIG_HOME");
1706 }
1707
1708 resultado.expect("import de .env válido deve funcionar");
1709 assert_eq!(config.keys.len(), 2, "Deve ter importado 2 chaves");
1710
1711 let valores: Vec<&str> = config.keys.iter().map(|c| c.value.as_str()).collect();
1712 assert!(valores.contains(&"ctx7sk-import-alpha"));
1713 assert!(valores.contains(&"ctx7sk-import-beta"));
1714 }
1715
1716 #[test]
1717 #[serial_test::serial]
1718 fn testa_cmd_keys_import_env_sem_chaves_retorna_err() {
1719 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1720 let arquivo_env = dir_temp.path().join("vazio.env");
1721 std::fs::write(&arquivo_env, "# apenas comentario\nOUTRA_VAR=valor\n")
1722 .expect("Deve escrever .env sem chaves");
1723
1724 unsafe {
1726 std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1727 }
1728
1729 let resultado = cmd_keys_import(&arquivo_env);
1730
1731 unsafe {
1732 std::env::remove_var("XDG_CONFIG_HOME");
1733 }
1734
1735 assert!(
1736 resultado.is_err(),
1737 "Import de .env sem chaves CONTEXT7_API deve retornar Err"
1738 );
1739 }
1740
1741 #[test]
1742 #[serial_test::serial]
1743 fn testa_cmd_keys_import_arquivo_inexistente_retorna_err() {
1744 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1745 let arquivo_inexistente = dir_temp.path().join("nao_existe.env");
1746
1747 unsafe {
1749 std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1750 }
1751
1752 let resultado = cmd_keys_import(&arquivo_inexistente);
1753
1754 unsafe {
1755 std::env::remove_var("XDG_CONFIG_HOME");
1756 }
1757
1758 assert!(
1759 resultado.is_err(),
1760 "Import de arquivo inexistente deve retornar Err"
1761 );
1762 }
1763
1764 #[test]
1765 #[serial_test::serial]
1766 fn testa_cmd_keys_import_roundtrip_add_depois_list() {
1767 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1768 let arquivo_env = dir_temp.path().join("roundtrip.env");
1769 std::fs::write(
1770 &arquivo_env,
1771 "CONTEXT7_API=ctx7sk-rtrip-01\nCONTEXT7_API=ctx7sk-rtrip-02\n",
1772 )
1773 .expect("Deve escrever .env de roundtrip");
1774
1775 unsafe {
1777 std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1778 }
1779
1780 cmd_keys_import(&arquivo_env).expect("Import deve funcionar");
1781
1782 let config = ler_config_xdg_raw()
1783 .expect("Deve retornar Ok")
1784 .expect("Deve retornar Some após import");
1785
1786 unsafe {
1787 std::env::remove_var("XDG_CONFIG_HOME");
1788 }
1789
1790 assert_eq!(
1791 config.keys.len(),
1792 2,
1793 "Roundtrip: deve ter 2 chaves após import"
1794 );
1795 assert_eq!(config.keys[0].value, "ctx7sk-rtrip-01");
1796 assert_eq!(config.keys[1].value, "ctx7sk-rtrip-02");
1797 }
1798
1799 #[test]
1802 #[serial_test::serial]
1803 fn testa_cmd_keys_export_em_config_vazio_retorna_ok() {
1804 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1805 unsafe {
1807 std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1808 }
1809
1810 let resultado = cmd_keys_export();
1811
1812 unsafe {
1813 std::env::remove_var("XDG_CONFIG_HOME");
1814 }
1815
1816 assert!(
1817 resultado.is_ok(),
1818 "Export de config vazio deve retornar Ok, obteve: {:?}",
1819 resultado
1820 );
1821 }
1822
1823 #[test]
1824 #[serial_test::serial]
1825 fn testa_cmd_keys_export_retorna_ok_com_chaves_existentes() {
1826 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1827 unsafe {
1829 std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1830 }
1831
1832 escrever_config_xdg("ctx7sk-export-um").expect("Deve escrever chave 1");
1833 escrever_config_xdg("ctx7sk-export-dois").expect("Deve escrever chave 2");
1834
1835 let resultado = cmd_keys_export();
1836
1837 unsafe {
1838 std::env::remove_var("XDG_CONFIG_HOME");
1839 }
1840
1841 assert!(
1842 resultado.is_ok(),
1843 "Export com chaves existentes deve retornar Ok, obteve: {:?}",
1844 resultado
1845 );
1846 }
1847
1848 #[test]
1851 #[serial_test::serial]
1852 fn testa_ler_config_xdg_raw_retorna_none_sem_arquivo() {
1853 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1854 unsafe {
1856 std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1857 }
1858
1859 let resultado = ler_config_xdg_raw();
1860
1861 unsafe {
1862 std::env::remove_var("XDG_CONFIG_HOME");
1863 }
1864
1865 let valor = resultado.expect("Deve retornar Ok");
1866 assert!(
1867 valor.is_none(),
1868 "Deve retornar None quando config.toml não existe"
1869 );
1870 }
1871
1872 #[test]
1873 #[serial_test::serial]
1874 fn testa_ler_config_xdg_raw_retorna_config_com_chaves() {
1875 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1876 let dir_context7 = dir_temp.path().join("context7");
1877 std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
1878
1879 let toml = r#"schema_version = 1
1880
1881[[keys]]
1882value = "ctx7sk-raw-01"
1883added_at = "2026-04-08T00:00:00+00:00"
1884
1885[[keys]]
1886value = "ctx7sk-raw-02"
1887added_at = "2026-04-08T00:01:00+00:00"
1888"#;
1889 std::fs::write(dir_context7.join("config.toml"), toml).expect("Deve escrever config.toml");
1890
1891 unsafe {
1893 std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1894 }
1895
1896 let resultado = ler_config_xdg_raw();
1897
1898 unsafe {
1899 std::env::remove_var("XDG_CONFIG_HOME");
1900 }
1901
1902 let config = resultado
1903 .expect("Deve retornar Ok")
1904 .expect("Deve retornar Some com config");
1905 assert_eq!(config.keys.len(), 2);
1906 assert_eq!(config.keys[0].value, "ctx7sk-raw-01");
1907 assert_eq!(config.keys[1].value, "ctx7sk-raw-02");
1908 }
1909
1910 #[test]
1911 #[serial_test::serial]
1912 fn testa_cmd_keys_path_retorna_ok() {
1913 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1914 unsafe {
1916 std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1917 }
1918
1919 let resultado = cmd_keys_path();
1920
1921 unsafe {
1922 std::env::remove_var("XDG_CONFIG_HOME");
1923 }
1924
1925 resultado.expect("cmd_keys_path deve retornar Ok");
1926 }
1927
1928 #[test]
1929 #[serial_test::serial]
1930 fn testa_descobrir_caminho_config_termina_com_config_toml() {
1931 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1932 unsafe {
1934 std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1935 }
1936
1937 let caminho = descobrir_caminho_config();
1938
1939 unsafe {
1940 std::env::remove_var("XDG_CONFIG_HOME");
1941 }
1942
1943 let caminho = caminho.expect("Deve retornar caminho XDG válido");
1944 assert!(
1945 caminho.to_string_lossy().ends_with("config.toml"),
1946 "Caminho deve terminar com config.toml, obteve: {}",
1947 caminho.display()
1948 );
1949 assert!(
1950 caminho.to_string_lossy().contains("context7"),
1951 "Caminho deve conter 'context7', obteve: {}",
1952 caminho.display()
1953 );
1954 }
1955
1956 #[test]
1959 #[serial_test::serial]
1960 fn testa_fluxo_completo_add_list_remove_clear() {
1961 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1962 unsafe {
1964 std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1965 }
1966
1967 cmd_keys_add("ctx7sk-fluxo-01").expect("Add 1 deve funcionar");
1968 cmd_keys_add("ctx7sk-fluxo-02").expect("Add 2 deve funcionar");
1969 cmd_keys_add("ctx7sk-fluxo-03").expect("Add 3 deve funcionar");
1970
1971 let config_antes = ler_config_xdg_raw()
1972 .expect("Ok")
1973 .expect("Some com 3 chaves");
1974 assert_eq!(config_antes.keys.len(), 3, "Deve ter 3 chaves após 3 adds");
1975
1976 cmd_keys_remove(2).expect("Remove índice 2 deve funcionar");
1977
1978 let config_pos_remove = ler_config_xdg_raw()
1979 .expect("Ok")
1980 .expect("Some com 2 chaves");
1981 assert_eq!(
1982 config_pos_remove.keys.len(),
1983 2,
1984 "Deve ter 2 chaves após remove"
1985 );
1986 assert_eq!(config_pos_remove.keys[0].value, "ctx7sk-fluxo-01");
1987 assert_eq!(config_pos_remove.keys[1].value, "ctx7sk-fluxo-03");
1988
1989 cmd_keys_clear(true).expect("Clear com yes=true deve funcionar");
1990
1991 let caminho = descobrir_caminho_config().expect("Deve ter caminho");
1992 let config_final = ler_config_toml_do_caminho(&caminho).expect("Deve ler config final");
1993
1994 unsafe {
1995 std::env::remove_var("XDG_CONFIG_HOME");
1996 }
1997
1998 assert!(
1999 config_final.keys.is_empty(),
2000 "Após clear, chaves devem estar vazias"
2001 );
2002 }
2003}