1use anyhow::{bail, Context, Result};
9use chrono::Utc;
10use directories::ProjectDirs;
11use serde::{Deserialize, Serialize};
12use std::path::PathBuf;
13
14use zeroize::{Zeroize, ZeroizeOnDrop};
15
16use unicode_normalization::UnicodeNormalization;
17
18use crate::errors::ErroContext7;
19use crate::i18n::{t, Mensagem};
20
21#[derive(Debug, Serialize, Deserialize, Clone)]
27pub struct ChaveArmazenada {
28 pub value: String,
30 pub added_at: String,
32}
33
34#[derive(Debug, Serialize, Deserialize, Default)]
38pub struct ConfigArquivo {
39 pub schema_version: u32,
41 #[serde(default)]
43 pub keys: Vec<ChaveArmazenada>,
44}
45
46#[derive(Clone, Zeroize, ZeroizeOnDrop)]
54pub struct ChaveApi(String);
55
56impl ChaveApi {
57 pub fn new(valor: String) -> Self {
59 Self(valor)
60 }
61
62 pub fn valor(&self) -> &str {
64 &self.0
65 }
66}
67
68impl PartialEq<&str> for ChaveApi {
69 fn eq(&self, other: &&str) -> bool {
70 self.0 == *other
71 }
72}
73
74impl std::fmt::Debug for ChaveApi {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 write!(f, "ChaveApi({})", mascarar_chave(self.valor()))
77 }
78}
79
80impl std::fmt::Display for ChaveApi {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 write!(f, "{}", mascarar_chave(self.valor()))
83 }
84}
85
86pub fn aplicar_permissoes_600(caminho: &std::path::Path) -> Result<()> {
93 #[cfg(unix)]
94 {
95 use std::os::unix::fs::PermissionsExt;
96 let mut perms = std::fs::metadata(caminho)
97 .with_context(|| format!("Falha ao ler metadados de: {}", caminho.display()))?
98 .permissions();
99 perms.set_mode(0o600);
100 std::fs::set_permissions(caminho, perms)
101 .with_context(|| format!("Falha ao definir permissões em: {}", caminho.display()))?;
102 }
103 #[cfg(not(unix))]
104 let _ = caminho;
105 Ok(())
106}
107
108fn resolver_home_override() -> Option<PathBuf> {
116 let home = std::env::var("CONTEXT7_HOME").ok()?;
117 if home.is_empty() {
118 return None;
119 }
120 let base = PathBuf::from(&home);
121 if base
123 .components()
124 .any(|c| c == std::path::Component::ParentDir)
125 {
126 tracing::warn!(
127 "CONTEXT7_HOME='{}' rejeitado (path traversal) — usando padrão XDG",
128 home
129 );
130 return None;
131 }
132 let nomes_reservados = [
134 "CON", "PRN", "AUX", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7",
135 "COM8", "COM9", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8",
136 "LPT9",
137 ];
138 for componente in base.components() {
139 if let std::path::Component::Normal(nome) = componente {
140 let nome_upper = nome.to_string_lossy().to_uppercase();
141 let nome_base = nome_upper.split('.').next().unwrap_or("");
143 if nomes_reservados.contains(&nome_base) {
144 tracing::warn!(
145 "CONTEXT7_HOME='{}' rejeitado (nome reservado Windows '{}') — usando padrão XDG",
146 home, nome_base
147 );
148 return None;
149 }
150 }
151 }
152 Some(base)
153}
154
155pub fn descobrir_caminho_config() -> Option<PathBuf> {
162 let caminho = if let Some(base) = resolver_home_override() {
163 base.join("context7").join("config.toml")
164 } else {
165 ProjectDirs::from("", "", "context7")?
166 .config_dir()
167 .join("config.toml")
168 };
169 let caminho_str = caminho.to_string_lossy().nfc().collect::<String>();
171 Some(PathBuf::from(caminho_str))
172}
173
174pub fn descobrir_caminho_logs_xdg() -> Option<PathBuf> {
181 let caminho = if let Some(base) = resolver_home_override() {
182 base.join("context7").join("logs")
183 } else {
184 let dirs = ProjectDirs::from("", "", "context7")?;
185 #[cfg(target_os = "linux")]
187 {
188 dirs.state_dir()
189 .unwrap_or_else(|| dirs.data_local_dir())
190 .to_path_buf()
191 }
192 #[cfg(not(target_os = "linux"))]
193 {
194 dirs.data_local_dir().to_path_buf()
195 }
196 };
197 let caminho_str = caminho.to_string_lossy().nfc().collect::<String>();
199 Some(PathBuf::from(caminho_str))
200}
201
202pub fn ler_env_var_chave() -> Option<Vec<String>> {
211 std::env::var("CONTEXT7_API_KEYS")
212 .ok()
213 .map(|valor| {
214 let estimativa = valor.matches(',').count() + 1;
215 let mut chaves = Vec::with_capacity(estimativa);
216 for s in valor.split(',') {
217 let trimmed = s.trim().to_string();
218 if !trimmed.is_empty() {
219 chaves.push(trimmed);
220 }
221 }
222 chaves
223 })
224 .filter(|v| !v.is_empty())
225}
226
227pub fn ler_config_xdg() -> Result<Option<Vec<String>>> {
232 let caminho = match descobrir_caminho_config() {
233 Some(p) => p,
234 None => return Ok(None),
235 };
236
237 if !caminho.exists() {
238 return Ok(None);
239 }
240
241 let conteudo = std::fs::read_to_string(&caminho)
242 .with_context(|| format!("Falha ao ler configuração XDG em: {}", caminho.display()))?;
243
244 let config: ConfigArquivo = toml::from_str(&conteudo)
245 .with_context(|| format!("TOML inválido em: {}", caminho.display()))?;
246
247 let chaves: Vec<String> = config
248 .keys
249 .into_iter()
250 .map(|c| c.value)
251 .filter(|v| !v.is_empty())
252 .collect();
253
254 if chaves.is_empty() {
255 Ok(None)
256 } else {
257 Ok(Some(chaves))
258 }
259}
260
261pub fn ler_env_cwd() -> Option<Vec<String>> {
266 let caminho = std::env::current_dir().ok().map(|d| d.join(".env"))?;
267
268 if !caminho.exists() {
269 return None;
270 }
271
272 std::fs::read_to_string(&caminho)
273 .ok()
274 .and_then(|conteudo| extrair_chaves_env(&conteudo).ok())
275}
276
277pub fn ler_env_compile_time() -> Option<Vec<String>> {
288 option_env!("CONTEXT7_API_KEYS").map(|valor| {
289 let estimativa = valor.matches(',').count() + 1;
290 let mut chaves = Vec::with_capacity(estimativa);
291 for s in valor.split(',') {
292 let trimmed = s.trim().to_string();
293 if !trimmed.is_empty() {
294 chaves.push(trimmed);
295 }
296 }
297 chaves
298 })
299}
300
301pub fn carregar_chaves_api() -> Result<Vec<ChaveApi>> {
310 use tracing::{info, warn};
311
312 if let Some(chaves) = ler_env_var_chave() {
314 info!("Chaves carregadas via variável de ambiente CONTEXT7_API_KEYS");
315 return Ok(chaves.into_iter().map(ChaveApi::new).collect());
316 }
317
318 match ler_config_xdg() {
320 Ok(Some(chaves)) => {
321 info!("Chaves carregadas via configuração XDG");
322 return Ok(chaves.into_iter().map(ChaveApi::new).collect());
323 }
324 Ok(None) => {}
325 Err(e) => {
326 warn!("Falha ao ler configuração XDG (continuando): {}", e);
327 }
328 }
329
330 if let Some(chaves) = ler_env_cwd() {
332 info!(
333 "Iniciando context7 com {} chaves de API disponíveis",
334 chaves.len()
335 );
336 return Ok(chaves.into_iter().map(ChaveApi::new).collect());
337 }
338
339 if let Some(chaves) = ler_env_compile_time() {
341 info!("Chaves carregadas via compile-time CONTEXT7_API_KEYS");
342 return Ok(chaves.into_iter().map(ChaveApi::new).collect());
343 }
344
345 bail!(t(Mensagem::NenhumaChaveConfigurada))
346}
347
348pub fn escrever_config_xdg(nova_chave: &str) -> Result<PathBuf> {
355 let caminho = descobrir_caminho_config()
356 .context("Sistema não suporta diretórios XDG — impossível salvar configuração")?;
357
358 if let Some(pai) = caminho.parent() {
360 std::fs::create_dir_all(pai)
361 .with_context(|| format!("Falha ao criar diretório: {}", pai.display()))?;
362 }
363
364 let mut config = if caminho.exists() {
366 let conteudo = std::fs::read_to_string(&caminho)
367 .with_context(|| format!("Falha ao ler config existente: {}", caminho.display()))?;
368 toml::from_str::<ConfigArquivo>(&conteudo)
369 .with_context(|| format!("TOML inválido em: {}", caminho.display()))?
370 } else {
371 ConfigArquivo {
372 schema_version: 1,
373 keys: Vec::new(),
374 }
375 };
376
377 let ja_existe = config.keys.iter().any(|c| c.value == nova_chave);
379 if !ja_existe {
380 config.keys.push(ChaveArmazenada {
381 value: nova_chave.to_string(),
382 added_at: Utc::now().to_rfc3339(),
383 });
384 }
385
386 let toml_str =
388 toml::to_string_pretty(&config).context("Falha ao serializar configuração para TOML")?;
389 std::fs::write(&caminho, &toml_str)
390 .with_context(|| format!("Falha ao escrever config em: {}", caminho.display()))?;
391
392 aplicar_permissoes_600(&caminho)?;
393
394 Ok(caminho)
395}
396
397pub fn ler_config_xdg_raw() -> Result<Option<ConfigArquivo>> {
402 let caminho = match descobrir_caminho_config() {
403 Some(p) => p,
404 None => return Ok(None),
405 };
406
407 if !caminho.exists() {
408 return Ok(None);
409 }
410
411 let conteudo = std::fs::read_to_string(&caminho)
412 .with_context(|| format!("Falha ao ler configuração XDG em: {}", caminho.display()))?;
413
414 let config: ConfigArquivo = toml::from_str(&conteudo)
415 .with_context(|| format!("TOML inválido em: {}", caminho.display()))?;
416
417 Ok(Some(config))
418}
419
420pub fn escrever_config_arquivo(config: &ConfigArquivo) -> Result<PathBuf> {
425 let caminho = descobrir_caminho_config()
426 .context("Sistema não suporta diretórios XDG — impossível salvar configuração")?;
427
428 if let Some(pai) = caminho.parent() {
429 std::fs::create_dir_all(pai)
430 .with_context(|| format!("Falha ao criar diretório: {}", pai.display()))?;
431 }
432
433 let toml_str =
434 toml::to_string_pretty(config).context("Falha ao serializar configuração para TOML")?;
435 std::fs::write(&caminho, &toml_str)
436 .with_context(|| format!("Falha ao escrever config em: {}", caminho.display()))?;
437
438 aplicar_permissoes_600(&caminho)?;
439
440 Ok(caminho)
441}
442
443pub fn mascarar_chave(chave: &str) -> String {
452 let n_chars = chave.chars().count();
453 let inicio = 12;
454 let fim = 4;
455 if n_chars <= inicio + fim {
456 return "***".to_string();
457 }
458 let prefixo: String = chave.chars().take(inicio).collect();
459 let sufixo: String = chave
460 .chars()
461 .rev()
462 .take(fim)
463 .collect::<String>()
464 .chars()
465 .rev()
466 .collect();
467 format!("{}...{}", prefixo, sufixo)
468}
469
470pub fn extrair_chaves_env(conteudo: &str) -> Result<Vec<String>> {
476 let chaves: Vec<String> = conteudo
477 .lines()
478 .filter_map(|linha| {
479 let linha_sem_comentario = linha.split('#').next().unwrap_or("").trim();
481 linha_sem_comentario
482 .strip_prefix("CONTEXT7_API=")
483 .map(|valor| {
484 valor
486 .trim()
487 .trim_matches('"')
488 .trim_matches('\'')
489 .to_string()
490 })
491 .filter(|v| !v.is_empty())
492 })
493 .collect();
494
495 if chaves.is_empty() {
496 bail!(t(Mensagem::NenhumaChaveContext7NoArquivo));
497 }
498
499 Ok(chaves)
500}
501
502pub fn cmd_keys_add(chave: &str) -> Result<()> {
509 let chave_trimmed = chave.trim();
510 if chave_trimmed.is_empty() {
511 crate::output::exibir_chave_invalida_vazia();
512 bail!(ErroContext7::OperacaoKeysFalhou);
513 }
514 if !chave_trimmed.starts_with("ctx7sk-") || chave_trimmed.len() < 16 {
515 crate::output::exibir_aviso_formato_chave();
516 }
517 if let Some(config) = ler_config_xdg_raw()? {
519 if config.keys.iter().any(|c| c.value == chave_trimmed) {
520 crate::output::exibir_chave_ja_existia();
521 return Ok(());
522 }
523 }
524 let caminho = escrever_config_xdg(chave_trimmed)?;
525 crate::output::exibir_chave_adicionada(&caminho);
526 Ok(())
527}
528
529pub fn cmd_keys_list(json: bool) -> Result<()> {
533 match ler_config_xdg_raw()? {
534 None => {
535 if json {
536 crate::output::exibir_json_array_vazio();
537 } else {
538 crate::output::exibir_nenhuma_chave();
539 }
540 }
541 Some(config) if config.keys.is_empty() => {
542 if json {
543 crate::output::exibir_json_array_vazio();
544 } else {
545 crate::output::exibir_nenhuma_chave();
546 }
547 }
548 Some(config) => {
549 if json {
550 let mut mascaradas: Vec<serde_json::Value> = Vec::with_capacity(config.keys.len());
551 mascaradas.extend(config.keys.iter().enumerate().map(|(i, k)| {
552 serde_json::json!({
553 "index": i + 1,
554 "masked_key": mascarar_chave(&k.value),
555 "added_at": crate::output::formatar_added_at_display(&k.added_at)
556 })
557 }));
558 crate::output::exibir_json_bruto(
559 &serde_json::to_string_pretty(&mascaradas).with_context(|| {
560 crate::i18n::t(crate::i18n::Mensagem::FalhaSerializarJson)
561 })?,
562 );
563 } else {
564 crate::output::exibir_chaves_mascaradas(&config.keys, mascarar_chave);
565 }
566 }
567 }
568 Ok(())
569}
570
571pub fn cmd_keys_remove(indice: usize) -> Result<()> {
573 let mut config = match ler_config_xdg_raw()? {
574 None => {
575 crate::output::exibir_nenhuma_chave_para_remover();
576 bail!(ErroContext7::OperacaoKeysFalhou);
577 }
578 Some(c) if c.keys.is_empty() => {
579 crate::output::exibir_nenhuma_chave_para_remover();
580 bail!(ErroContext7::OperacaoKeysFalhou);
581 }
582 Some(c) => c,
583 };
584
585 if indice == 0 || indice > config.keys.len() {
586 crate::output::exibir_indice_invalido(indice, config.keys.len());
587 bail!(ErroContext7::OperacaoKeysFalhou);
588 }
589
590 let removida = config.keys.remove(indice - 1);
591 escrever_config_arquivo(&config)?;
592 crate::output::exibir_chave_removida(&mascarar_chave(&removida.value));
593 Ok(())
594}
595
596pub fn cmd_keys_clear(sim: bool) -> Result<()> {
598 if !sim && !crate::output::confirmar_clear()? {
599 crate::output::exibir_operacao_cancelada();
600 return Ok(());
601 }
602
603 let config = ConfigArquivo {
604 schema_version: 1,
605 keys: Vec::new(),
606 };
607 escrever_config_arquivo(&config)?;
608 crate::output::exibir_chaves_removidas();
609 Ok(())
610}
611
612pub fn cmd_keys_path() -> Result<()> {
614 match descobrir_caminho_config() {
615 Some(caminho) => crate::output::exibir_caminho_config(&caminho),
616 None => crate::output::exibir_xdg_nao_suportado(),
617 }
618 Ok(())
619}
620
621pub fn cmd_keys_import(arquivo: &std::path::Path) -> Result<()> {
625 let conteudo = std::fs::read_to_string(arquivo)
626 .with_context(|| format!("Falha ao ler arquivo: {}", arquivo.display()))?;
627
628 let chaves =
629 extrair_chaves_env(&conteudo).with_context(|| format!("Arquivo: {}", arquivo.display()))?;
630
631 let total = chaves.len();
632 let mut importadas = 0usize;
633
634 for chave in &chaves {
635 escrever_config_xdg(chave)?;
636 importadas += 1;
637 }
638
639 crate::output::exibir_importacao_concluida(importadas, total);
640 Ok(())
641}
642
643pub fn cmd_keys_export() -> Result<()> {
647 match ler_config_xdg_raw()? {
648 None => {}
649 Some(config) if config.keys.is_empty() => {}
650 Some(config) => {
651 for chave in &config.keys {
652 crate::output::exibir_chave_exportada(&chave.value);
653 }
654 }
655 }
656 Ok(())
657}
658
659#[cfg(test)]
662mod testes {
663 use super::*;
664
665 fn ler_config_toml_do_caminho(caminho: &std::path::Path) -> Result<ConfigArquivo> {
669 let conteudo = std::fs::read_to_string(caminho)
670 .with_context(|| format!("Falha ao ler: {}", caminho.display()))?;
671 toml::from_str(&conteudo)
672 .with_context(|| format!("TOML inválido em: {}", caminho.display()))
673 }
674
675 #[test]
678 fn testa_parsing_env_com_multiplas_chaves_iguais() {
679 let mut conteudo = String::new();
680 for i in 0..17 {
681 conteudo.push_str(&format!("CONTEXT7_API=ctx7sk-chave-{:02}\n", i));
682 }
683 let chaves = extrair_chaves_env(&conteudo).expect("Deve extrair 17 chaves sem erro");
684 assert_eq!(chaves.len(), 17, "Deve retornar exatamente 17 chaves");
685 for (i, chave) in chaves.iter().enumerate() {
686 assert_eq!(
687 chave,
688 &format!("ctx7sk-chave-{:02}", i),
689 "Chave {} deve ter o valor correto",
690 i
691 );
692 }
693 }
694
695 #[test]
696 fn testa_parsing_env_ignora_comentarios_e_linhas_vazias() {
697 let conteudo = "# Este é um comentário\n\
698 CONTEXT7_API=ctx7sk-chave-valida-01\n\
699 \n\
700 # Outro comentário\n\
701 CONTEXT7_API=ctx7sk-chave-valida-02\n\
702 \n";
703 let chaves = extrair_chaves_env(conteudo).expect("Deve extrair chaves sem erro");
704 assert_eq!(chaves.len(), 2, "Deve ignorar comentários e linhas vazias");
705 assert_eq!(chaves[0], "ctx7sk-chave-valida-01");
706 assert_eq!(chaves[1], "ctx7sk-chave-valida-02");
707 }
708
709 #[test]
710 fn testa_parsing_env_remove_aspas_duplas() {
711 let conteudo = "CONTEXT7_API=\"ctx7sk-abc-com-aspas\"\n";
712 let chaves = extrair_chaves_env(conteudo).expect("Deve extrair chave sem erro");
713 assert_eq!(chaves.len(), 1);
714 assert_eq!(
715 chaves[0], "ctx7sk-abc-com-aspas",
716 "Deve remover aspas duplas"
717 );
718 }
719
720 #[test]
721 fn testa_parsing_env_remove_aspas_simples() {
722 let conteudo = "CONTEXT7_API='ctx7sk-abc-aspas-simples'\n";
723 let chaves = extrair_chaves_env(conteudo).expect("Deve extrair chave sem erro");
724 assert_eq!(chaves.len(), 1);
725 assert_eq!(
726 chaves[0], "ctx7sk-abc-aspas-simples",
727 "Deve remover aspas simples"
728 );
729 }
730
731 #[test]
732 fn testa_parsing_env_erro_quando_nenhuma_chave() {
733 let conteudo = "# Apenas comentários\n\
734 OUTRA_VAR=valor\n\
735 \n";
736 let resultado = extrair_chaves_env(conteudo);
737 assert!(
738 resultado.is_err(),
739 "Deve retornar Err quando não há chaves CONTEXT7_API"
740 );
741 let mensagem_erro = resultado.unwrap_err().to_string();
742 assert!(
743 mensagem_erro.contains("chave")
744 || mensagem_erro.contains("CONTEXT7_API")
745 || mensagem_erro.contains("key")
746 || mensagem_erro.contains("API"),
747 "Mensagem de erro deve mencionar CONTEXT7_API, chave, key ou API, obteve: {}",
748 mensagem_erro
749 );
750 }
751
752 #[test]
753 fn testa_parsing_env_ignora_chaves_vazias() {
754 let conteudo = "CONTEXT7_API=\n\
755 CONTEXT7_API=ctx7sk-valida\n";
756 let chaves = extrair_chaves_env(conteudo).expect("Deve extrair chave sem erro");
757 assert_eq!(
758 chaves.len(),
759 1,
760 "Deve ignorar entradas CONTEXT7_API sem valor"
761 );
762 assert_eq!(chaves[0], "ctx7sk-valida");
763 }
764
765 #[test]
766 fn testa_parsing_env_ignora_comentario_inline() {
767 let conteudo = "CONTEXT7_API=ctx7sk-valida # comentário aqui\n";
768 let chaves = extrair_chaves_env(conteudo).expect("Deve extrair chave sem erro");
769 assert_eq!(chaves.len(), 1);
770 assert_eq!(chaves[0], "ctx7sk-valida");
771 }
772
773 #[test]
776 fn testa_parsing_env_com_line_endings_crlf() {
777 let conteudo = "CONTEXT7_API=ctx7sk-crlf-chave-a\r\nCONTEXT7_API=ctx7sk-crlf-chave-b\r\n";
779 let chaves =
780 extrair_chaves_env(conteudo).expect("Deve extrair 2 chaves de conteúdo CRLF sem erro");
781 assert_eq!(
782 chaves.len(),
783 2,
784 "Deve retornar exatamente 2 chaves com CRLF"
785 );
786 assert_eq!(
787 chaves[0], "ctx7sk-crlf-chave-a",
788 "Primeira chave não deve conter \\r residual"
789 );
790 assert_eq!(
791 chaves[1], "ctx7sk-crlf-chave-b",
792 "Segunda chave não deve conter \\r residual"
793 );
794 }
795
796 #[test]
797 fn testa_parsing_env_com_line_endings_mixed() {
798 let conteudo = "CONTEXT7_API=ctx7sk-mixed-chave-a\nCONTEXT7_API=ctx7sk-mixed-chave-b\r\n";
800 let chaves = extrair_chaves_env(conteudo)
801 .expect("Deve extrair 2 chaves de conteúdo misto LF/CRLF sem erro");
802 assert_eq!(
803 chaves.len(),
804 2,
805 "Deve retornar exatamente 2 chaves com line endings mistos"
806 );
807 assert_eq!(
808 chaves[0], "ctx7sk-mixed-chave-a",
809 "Chave com LF não deve ter \\r residual"
810 );
811 assert_eq!(
812 chaves[1], "ctx7sk-mixed-chave-b",
813 "Chave com CRLF não deve ter \\r residual"
814 );
815 }
816
817 #[test]
820 fn testa_mascarar_chave_com_valor_longo_exibe_prefixo_e_sufixo() {
821 let chave = "ctx7sk-abc123-def456-ghi789";
822 assert_eq!(chave.len(), 27, "Pré-condição: chave deve ter 27 chars");
823 let mascarada = mascarar_chave(chave);
824 assert!(
825 mascarada.starts_with("ctx7sk-abc12"),
826 "Deve iniciar com os primeiros 12 chars, obteve: {}",
827 mascarada
828 );
829 assert!(
830 mascarada.ends_with("i789"),
831 "Deve terminar com os últimos 4 chars, obteve: {}",
832 mascarada
833 );
834 assert!(
835 mascarada.contains("..."),
836 "Deve conter '...' entre prefixo e sufixo, obteve: {}",
837 mascarada
838 );
839 }
840
841 #[test]
842 fn testa_mascarar_chave_curta_retorna_asteriscos() {
843 let chave_exatamente_16 = "ctx7sk-abcdef012";
844 assert_eq!(
845 chave_exatamente_16.len(),
846 16,
847 "Pré-condição: chave deve ter 16 chars"
848 );
849 let mascarada = mascarar_chave(chave_exatamente_16);
850 assert_eq!(
851 mascarada, "***",
852 "Chave de 16 chars deve retornar '***', obteve: {}",
853 mascarada
854 );
855 }
856
857 #[test]
858 fn testa_mascarar_chave_vazia_retorna_asteriscos() {
859 let mascarada = mascarar_chave("");
860 assert_eq!(
861 mascarada, "***",
862 "Chave vazia deve retornar '***', obteve: {}",
863 mascarada
864 );
865 }
866
867 #[test]
868 fn testa_mascarar_chave_de_exatamente_17_chars_mascara_corretamente() {
869 let chave = "ctx7sk-abcdef0123"; assert_eq!(chave.len(), 17, "Pré-condição: chave deve ter 17 chars");
871 let mascarada = mascarar_chave(chave);
872 assert!(
873 mascarada.contains("..."),
874 "Chave de 17 chars deve ser mascarada, obteve: {}",
875 mascarada
876 );
877 assert_eq!(
878 &mascarada[..12],
879 &chave[..12],
880 "Prefixo de 12 chars deve ser preservado"
881 );
882 assert!(
883 mascarada.ends_with(&chave[chave.len() - 4..]),
884 "Sufixo de 4 chars deve ser preservado"
885 );
886 }
887
888 #[test]
891 #[serial_test::serial]
892 fn testa_ler_env_var_chave_retorna_some_quando_setada() {
893 unsafe {
896 std::env::set_var("CONTEXT7_API_KEYS", "ctx7sk-chave-teste-01");
897 }
898 let resultado = ler_env_var_chave();
899 unsafe {
900 std::env::remove_var("CONTEXT7_API_KEYS");
901 }
902
903 let chaves = resultado.expect("Deve retornar Some com chave válida");
904 assert_eq!(chaves.len(), 1, "Deve retornar exatamente 1 chave");
905 assert_eq!(chaves[0], "ctx7sk-chave-teste-01");
906 }
907
908 #[test]
909 #[serial_test::serial]
910 fn testa_ler_env_var_chave_aceita_multiplas_separadas_por_virgula() {
911 unsafe {
913 std::env::set_var(
914 "CONTEXT7_API_KEYS",
915 "ctx7sk-chave-a, ctx7sk-chave-b , ctx7sk-chave-c",
916 );
917 }
918 let resultado = ler_env_var_chave();
919 unsafe {
920 std::env::remove_var("CONTEXT7_API_KEYS");
921 }
922
923 let chaves = resultado.expect("Deve retornar Some com múltiplas chaves");
924 assert_eq!(chaves.len(), 3, "Deve retornar 3 chaves");
925 assert_eq!(chaves[0], "ctx7sk-chave-a");
926 assert_eq!(chaves[1], "ctx7sk-chave-b");
927 assert_eq!(chaves[2], "ctx7sk-chave-c");
928 }
929
930 #[test]
931 #[serial_test::serial]
932 fn testa_ler_env_var_chave_retorna_none_quando_vazia() {
933 unsafe {
935 std::env::set_var("CONTEXT7_API_KEYS", "");
936 }
937 let resultado = ler_env_var_chave();
938 unsafe {
939 std::env::remove_var("CONTEXT7_API_KEYS");
940 }
941
942 assert!(
943 resultado.is_none(),
944 "Deve retornar None quando env var está vazia"
945 );
946 }
947
948 #[test]
949 #[serial_test::serial]
950 fn testa_ler_env_var_chave_retorna_none_quando_apenas_whitespace() {
951 unsafe {
953 std::env::set_var("CONTEXT7_API_KEYS", " , , ");
954 }
955 let resultado = ler_env_var_chave();
956 unsafe {
957 std::env::remove_var("CONTEXT7_API_KEYS");
958 }
959
960 assert!(
961 resultado.is_none(),
962 "Deve retornar None quando env var contém apenas whitespace/vírgulas"
963 );
964 }
965
966 #[test]
967 #[serial_test::serial]
968 fn testa_ler_env_var_chave_retorna_none_quando_ausente() {
969 unsafe {
971 std::env::remove_var("CONTEXT7_API_KEYS");
972 }
973 let resultado = ler_env_var_chave();
974
975 assert!(
976 resultado.is_none(),
977 "Deve retornar None quando env var não existe"
978 );
979 }
980
981 #[test]
984 #[serial_test::serial]
985 fn testa_context7_home_rejeita_path_traversal() {
986 let casos = ["../../../etc", "..", "/tmp/../etc"];
987 for caso in &casos {
988 unsafe {
990 std::env::set_var("CONTEXT7_HOME", caso);
991 }
992 let resultado = descobrir_caminho_config();
993 unsafe {
994 std::env::remove_var("CONTEXT7_HOME");
995 }
996
997 if let Some(caminho) = resultado {
999 let s = caminho.to_string_lossy();
1000 assert!(
1001 !s.contains(".."),
1002 "Path traversal '{caso}' não deve resultar em caminho com '..': {s}"
1003 );
1004 }
1005 }
1007 }
1008
1009 #[test]
1012 #[serial_test::serial]
1013 fn testa_ler_config_xdg_arquivo_inexistente_retorna_none() {
1014 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1015 unsafe {
1017 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1018 }
1019 let resultado = ler_config_xdg();
1020 unsafe {
1021 std::env::remove_var("CONTEXT7_HOME");
1022 }
1023
1024 let valor = resultado.expect("Deve retornar Ok quando arquivo não existe");
1025 assert!(
1026 valor.is_none(),
1027 "Deve retornar None quando config.toml não existe"
1028 );
1029 }
1030
1031 #[test]
1032 #[serial_test::serial]
1033 fn testa_ler_config_xdg_le_toml_valido_com_multiplas_chaves() {
1034 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1035 let dir_context7 = dir_temp.path().join("context7");
1036 std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
1037
1038 let toml_conteudo = r#"schema_version = 1
1039
1040[[keys]]
1041value = "ctx7sk-chave-xdg-01"
1042added_at = "2026-01-01T00:00:00+00:00"
1043
1044[[keys]]
1045value = "ctx7sk-chave-xdg-02"
1046added_at = "2026-01-02T00:00:00+00:00"
1047"#;
1048 std::fs::write(dir_context7.join("config.toml"), toml_conteudo)
1049 .expect("Deve escrever config.toml");
1050
1051 unsafe {
1053 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1054 }
1055 let resultado = ler_config_xdg();
1056 unsafe {
1057 std::env::remove_var("CONTEXT7_HOME");
1058 }
1059
1060 let chaves = resultado
1061 .expect("Deve retornar Ok")
1062 .expect("Deve retornar Some com chaves");
1063 assert_eq!(chaves.len(), 2, "Deve retornar 2 chaves");
1064 assert_eq!(chaves[0], "ctx7sk-chave-xdg-01");
1065 assert_eq!(chaves[1], "ctx7sk-chave-xdg-02");
1066 }
1067
1068 #[test]
1069 #[serial_test::serial]
1070 fn testa_ler_config_xdg_retorna_err_quando_toml_invalido() {
1071 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1072 let dir_context7 = dir_temp.path().join("context7");
1073 std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
1074
1075 std::fs::write(
1076 dir_context7.join("config.toml"),
1077 "schema_version = INVALIDO\n[[[malformado",
1078 )
1079 .expect("Deve escrever TOML inválido");
1080
1081 unsafe {
1083 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1084 }
1085 let resultado = ler_config_xdg();
1086 unsafe {
1087 std::env::remove_var("CONTEXT7_HOME");
1088 }
1089
1090 assert!(
1091 resultado.is_err(),
1092 "Deve retornar Err quando TOML está malformado"
1093 );
1094 }
1095
1096 #[test]
1097 #[serial_test::serial]
1098 fn testa_ler_config_xdg_preserva_ordem_das_chaves() {
1099 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1100 let dir_context7 = dir_temp.path().join("context7");
1101 std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
1102
1103 let toml_conteudo = r#"schema_version = 1
1104
1105[[keys]]
1106value = "ctx7sk-primeira"
1107added_at = "2026-01-01T00:00:00+00:00"
1108
1109[[keys]]
1110value = "ctx7sk-segunda"
1111added_at = "2026-01-02T00:00:00+00:00"
1112
1113[[keys]]
1114value = "ctx7sk-terceira"
1115added_at = "2026-01-03T00:00:00+00:00"
1116"#;
1117 std::fs::write(dir_context7.join("config.toml"), toml_conteudo)
1118 .expect("Deve escrever config.toml");
1119
1120 unsafe {
1122 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1123 }
1124 let resultado = ler_config_xdg();
1125 unsafe {
1126 std::env::remove_var("CONTEXT7_HOME");
1127 }
1128
1129 let chaves = resultado
1130 .expect("Deve retornar Ok")
1131 .expect("Deve retornar Some");
1132 assert_eq!(chaves[0], "ctx7sk-primeira");
1133 assert_eq!(chaves[1], "ctx7sk-segunda");
1134 assert_eq!(chaves[2], "ctx7sk-terceira");
1135 }
1136
1137 #[test]
1138 #[serial_test::serial]
1139 fn testa_ler_config_xdg_keys_vazio_retorna_none() {
1140 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1141 let dir_context7 = dir_temp.path().join("context7");
1142 std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
1143
1144 let toml_sem_chaves = "schema_version = 1\n";
1145 std::fs::write(dir_context7.join("config.toml"), toml_sem_chaves)
1146 .expect("Deve escrever config.toml sem keys");
1147
1148 unsafe {
1150 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1151 }
1152 let resultado = ler_config_xdg();
1153 unsafe {
1154 std::env::remove_var("CONTEXT7_HOME");
1155 }
1156
1157 let valor = resultado.expect("Deve retornar Ok");
1158 assert!(
1159 valor.is_none(),
1160 "Deve retornar None quando config.toml existe mas keys está vazio"
1161 );
1162 }
1163
1164 #[test]
1167 #[serial_test::serial]
1168 fn testa_escrever_config_xdg_roundtrip_serializa_e_deserializa() {
1169 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1170 unsafe {
1172 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1173 }
1174
1175 let caminho =
1176 escrever_config_xdg("ctx7sk-roundtrip-01").expect("Deve escrever config sem erro");
1177
1178 let config_lido = ler_config_toml_do_caminho(&caminho)
1179 .expect("Deve ler TOML escrito por escrever_config_xdg");
1180
1181 unsafe {
1182 std::env::remove_var("CONTEXT7_HOME");
1183 }
1184
1185 assert_eq!(config_lido.schema_version, 1, "schema_version deve ser 1");
1186 assert_eq!(config_lido.keys.len(), 1, "Deve conter 1 chave");
1187 assert_eq!(
1188 config_lido.keys[0].value, "ctx7sk-roundtrip-01",
1189 "Valor da chave deve ser preservado"
1190 );
1191 assert!(
1192 !config_lido.keys[0].added_at.is_empty(),
1193 "added_at não deve ser vazio"
1194 );
1195 }
1196
1197 #[test]
1198 #[serial_test::serial]
1199 fn testa_escrever_config_xdg_cria_diretorios_pai_se_nao_existirem() {
1200 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1201 let xdg_novo = dir_temp.path().join("xdg_inexistente");
1202 unsafe {
1204 std::env::set_var("CONTEXT7_HOME", &xdg_novo);
1205 }
1206
1207 let resultado = escrever_config_xdg("ctx7sk-mkdir-teste");
1208 unsafe {
1209 std::env::remove_var("CONTEXT7_HOME");
1210 }
1211
1212 let caminho = resultado.expect("Deve criar diretório pai e escrever config");
1213 assert!(
1214 caminho.exists(),
1215 "Arquivo de config deve existir após escrita"
1216 );
1217 }
1218
1219 #[test]
1220 #[serial_test::serial]
1221 fn testa_escrever_config_xdg_nao_duplica_chave_ja_existente() {
1222 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1223 unsafe {
1225 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1226 }
1227
1228 escrever_config_xdg("ctx7sk-unica").expect("Primeira escrita deve funcionar");
1229 escrever_config_xdg("ctx7sk-unica").expect("Segunda escrita não deve falhar");
1230
1231 let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1232 let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config");
1233
1234 unsafe {
1235 std::env::remove_var("CONTEXT7_HOME");
1236 }
1237
1238 assert_eq!(
1239 config.keys.len(),
1240 1,
1241 "Não deve duplicar chave já existente — deve ter apenas 1"
1242 );
1243 }
1244
1245 #[test]
1246 #[serial_test::serial]
1247 fn testa_escrever_config_xdg_acumula_chaves_distintas() {
1248 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1249 unsafe {
1251 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1252 }
1253
1254 escrever_config_xdg("ctx7sk-chave-a").expect("Primeira escrita deve funcionar");
1255 escrever_config_xdg("ctx7sk-chave-b").expect("Segunda escrita deve funcionar");
1256 escrever_config_xdg("ctx7sk-chave-c").expect("Terceira escrita deve funcionar");
1257
1258 let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1259 let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config");
1260
1261 unsafe {
1262 std::env::remove_var("CONTEXT7_HOME");
1263 }
1264
1265 assert_eq!(config.keys.len(), 3, "Deve acumular 3 chaves distintas");
1266 let valores: Vec<&str> = config.keys.iter().map(|c| c.value.as_str()).collect();
1267 assert!(valores.contains(&"ctx7sk-chave-a"));
1268 assert!(valores.contains(&"ctx7sk-chave-b"));
1269 assert!(valores.contains(&"ctx7sk-chave-c"));
1270 }
1271
1272 #[test]
1273 #[cfg(unix)]
1274 #[serial_test::serial]
1275 fn testa_escrever_config_xdg_aplica_permissoes_600_em_unix() {
1276 use std::os::unix::fs::PermissionsExt;
1277
1278 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1279 unsafe {
1281 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1282 }
1283
1284 let caminho =
1285 escrever_config_xdg("ctx7sk-perm-600").expect("Deve escrever config sem erro");
1286 unsafe {
1287 std::env::remove_var("CONTEXT7_HOME");
1288 }
1289
1290 let metadados = std::fs::metadata(&caminho).expect("Deve obter metadados do arquivo");
1291 let modo = metadados.permissions().mode() & 0o777;
1292
1293 assert_eq!(modo, 0o600, "Permissões devem ser 600, obteve: {:o}", modo);
1294 }
1295
1296 #[test]
1299 fn testa_config_arquivo_roundtrip_serde_preserva_todos_campos() {
1300 let config_original = ConfigArquivo {
1301 schema_version: 1,
1302 keys: vec![
1303 ChaveArmazenada {
1304 value: "ctx7sk-serde-01".to_string(),
1305 added_at: "2026-01-01T12:00:00+00:00".to_string(),
1306 },
1307 ChaveArmazenada {
1308 value: "ctx7sk-serde-02".to_string(),
1309 added_at: "2026-01-02T12:00:00+00:00".to_string(),
1310 },
1311 ],
1312 };
1313
1314 let toml_str = toml::to_string_pretty(&config_original)
1315 .expect("Deve serializar ConfigArquivo para TOML");
1316 let config_deserializado: ConfigArquivo =
1317 toml::from_str(&toml_str).expect("Deve deserializar TOML de volta para ConfigArquivo");
1318
1319 assert_eq!(
1320 config_deserializado.schema_version, config_original.schema_version,
1321 "schema_version deve ser preservado no roundtrip"
1322 );
1323 assert_eq!(
1324 config_deserializado.keys.len(),
1325 config_original.keys.len(),
1326 "Número de chaves deve ser preservado"
1327 );
1328 assert_eq!(
1329 config_deserializado.keys[0].value, config_original.keys[0].value,
1330 "Valor da primeira chave deve ser preservado"
1331 );
1332 assert_eq!(
1333 config_deserializado.keys[0].added_at, config_original.keys[0].added_at,
1334 "added_at da primeira chave deve ser preservado"
1335 );
1336 }
1337
1338 #[test]
1339 fn testa_config_arquivo_schema_version_sempre_presente_na_serializacao() {
1340 let config = ConfigArquivo {
1341 schema_version: 1,
1342 keys: Vec::new(),
1343 };
1344
1345 let toml_str = toml::to_string_pretty(&config).expect("Deve serializar para TOML");
1346
1347 assert!(
1348 toml_str.contains("schema_version"),
1349 "schema_version deve estar presente na serialização TOML"
1350 );
1351 assert!(toml_str.contains('1'), "Valor 1 deve estar presente");
1352 }
1353
1354 #[test]
1355 fn testa_config_arquivo_keys_vazio_aceito_na_deserializacao() {
1356 let toml_str = "schema_version = 1\n";
1357 let config: ConfigArquivo =
1358 toml::from_str(toml_str).expect("Deve deserializar com keys ausente (default vazio)");
1359
1360 assert_eq!(config.schema_version, 1);
1361 assert!(
1362 config.keys.is_empty(),
1363 "keys deve ser vazio quando não presente no TOML"
1364 );
1365 }
1366
1367 #[test]
1368 fn testa_chave_armazenada_preserva_added_at_como_string_utc() {
1369 let timestamp = "2026-04-08T20:00:00+00:00";
1370 let chave = ChaveArmazenada {
1371 value: "ctx7sk-timestamp".to_string(),
1372 added_at: timestamp.to_string(),
1373 };
1374
1375 let toml_str = toml::to_string_pretty(&chave).expect("Deve serializar ChaveArmazenada");
1376 let chave_de_volta: ChaveArmazenada =
1377 toml::from_str(&toml_str).expect("Deve deserializar ChaveArmazenada");
1378
1379 assert_eq!(
1380 chave_de_volta.added_at, timestamp,
1381 "Timestamp added_at deve ser preservado exatamente"
1382 );
1383 }
1384
1385 #[test]
1388 #[serial_test::serial]
1389 fn testa_carregar_chaves_api_env_var_tem_prioridade_sobre_xdg() {
1390 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1391 let dir_context7 = dir_temp.path().join("context7");
1392 std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
1393
1394 let toml_xdg = r#"schema_version = 1
1395[[keys]]
1396value = "ctx7sk-xdg-deve-ser-ignorada"
1397added_at = "2026-01-01T00:00:00+00:00"
1398"#;
1399 std::fs::write(dir_context7.join("config.toml"), toml_xdg)
1400 .expect("Deve escrever config XDG");
1401
1402 unsafe {
1404 std::env::set_var("CONTEXT7_API_KEYS", "ctx7sk-env-var-prioritaria");
1405 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1406 }
1407
1408 let resultado = carregar_chaves_api();
1409
1410 unsafe {
1411 std::env::remove_var("CONTEXT7_API_KEYS");
1412 std::env::remove_var("CONTEXT7_HOME");
1413 }
1414
1415 let chaves = resultado.expect("Deve carregar chaves via env var");
1416 assert_eq!(chaves.len(), 1);
1417 assert_eq!(
1418 chaves[0], "ctx7sk-env-var-prioritaria",
1419 "Env var deve ter prioridade sobre XDG"
1420 );
1421 }
1422
1423 #[test]
1424 #[serial_test::serial]
1425 fn testa_carregar_chaves_api_xdg_usado_quando_env_var_ausente() {
1426 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1427 let dir_context7 = dir_temp.path().join("context7");
1428 std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
1429
1430 let toml_xdg = r#"schema_version = 1
1431[[keys]]
1432value = "ctx7sk-via-xdg"
1433added_at = "2026-01-01T00:00:00+00:00"
1434"#;
1435 std::fs::write(dir_context7.join("config.toml"), toml_xdg)
1436 .expect("Deve escrever config XDG");
1437
1438 unsafe {
1440 std::env::remove_var("CONTEXT7_API_KEYS");
1441 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1442 }
1443
1444 let resultado = carregar_chaves_api();
1445
1446 unsafe {
1447 std::env::remove_var("CONTEXT7_HOME");
1448 }
1449
1450 let chaves = resultado.expect("Deve carregar chaves via XDG");
1451 assert_eq!(chaves.len(), 1);
1452 assert_eq!(chaves[0], "ctx7sk-via-xdg");
1453 }
1454
1455 #[test]
1456 #[serial_test::serial]
1457 fn testa_carregar_chaves_api_retorna_err_quando_nada_disponivel() {
1458 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1459 let dir_xdg_vazio = dir_temp.path().join("xdg_vazio");
1460 std::fs::create_dir_all(&dir_xdg_vazio).expect("Deve criar diretório XDG vazio");
1461
1462 let dir_sem_env = dir_temp.path().join("sem_env");
1463 std::fs::create_dir_all(&dir_sem_env).expect("Deve criar diretório sem .env");
1464
1465 unsafe {
1467 std::env::remove_var("CONTEXT7_API_KEYS");
1468 std::env::set_var("CONTEXT7_HOME", &dir_xdg_vazio);
1469 }
1470 let cwd_original = std::env::current_dir().expect("Deve obter CWD atual");
1471 std::env::set_current_dir(&dir_sem_env).expect("Deve mudar CWD");
1472
1473 let resultado = carregar_chaves_api();
1474
1475 std::env::set_current_dir(&cwd_original).expect("Deve restaurar CWD");
1476 unsafe {
1477 std::env::remove_var("CONTEXT7_HOME");
1478 }
1479
1480 assert!(
1481 resultado.is_err(),
1482 "Deve retornar Err quando nenhuma camada fornecer chaves"
1483 );
1484 }
1485
1486 #[test]
1487 #[serial_test::serial]
1488 fn testa_ler_env_cwd_le_env_com_multiplas_chaves_context7_api() {
1489 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1490 let conteudo_env = "CONTEXT7_API=ctx7sk-cwd-01\nCONTEXT7_API=ctx7sk-cwd-02\n";
1491 std::fs::write(dir_temp.path().join(".env"), conteudo_env)
1492 .expect("Deve escrever .env temporário");
1493
1494 let cwd_original = std::env::current_dir().expect("Deve obter CWD");
1495 std::env::set_current_dir(dir_temp.path()).expect("Deve mudar CWD para temp");
1496
1497 let resultado = ler_env_cwd();
1498
1499 std::env::set_current_dir(&cwd_original).expect("Deve restaurar CWD");
1500
1501 let chaves = resultado.expect("Deve retornar Some com chaves do .env CWD");
1502 assert_eq!(chaves.len(), 2, "Deve ler 2 chaves do .env");
1503 assert_eq!(chaves[0], "ctx7sk-cwd-01");
1504 assert_eq!(chaves[1], "ctx7sk-cwd-02");
1505 }
1506
1507 #[test]
1508 #[serial_test::serial]
1509 fn testa_ler_env_cwd_retorna_none_quando_env_ausente() {
1510 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1511
1512 let cwd_original = std::env::current_dir().expect("Deve obter CWD");
1513 std::env::set_current_dir(dir_temp.path()).expect("Deve mudar CWD para temp sem .env");
1514
1515 let resultado = ler_env_cwd();
1516
1517 std::env::set_current_dir(&cwd_original).expect("Deve restaurar CWD");
1518
1519 assert!(
1520 resultado.is_none(),
1521 "Deve retornar None quando não há .env no CWD"
1522 );
1523 }
1524
1525 #[test]
1526 fn testa_descobrir_caminho_logs_xdg_retorna_algum_caminho_valido() {
1527 let resultado = descobrir_caminho_logs_xdg();
1528
1529 if let Some(caminho) = resultado {
1530 let caminho_str = caminho.to_string_lossy();
1531 assert!(
1532 caminho_str.contains("context7"),
1533 "Caminho de logs XDG deve conter 'context7', obteve: {}",
1534 caminho_str
1535 );
1536 }
1537 }
1538
1539 #[test]
1540 #[serial_test::serial]
1541 fn testa_carregar_chaves_api_env_cwd_usado_quando_env_var_e_xdg_ausentes() {
1542 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1543 let dir_xdg_sem_config = dir_temp.path().join("xdg_sem_config");
1544 std::fs::create_dir_all(&dir_xdg_sem_config).expect("Deve criar diretório XDG vazio");
1545
1546 let dir_cwd = dir_temp.path().join("cwd_com_env");
1547 std::fs::create_dir_all(&dir_cwd).expect("Deve criar CWD temporário");
1548 std::fs::write(dir_cwd.join(".env"), "CONTEXT7_API=ctx7sk-cwd-camada-3\n")
1549 .expect("Deve escrever .env no CWD");
1550
1551 unsafe {
1553 std::env::remove_var("CONTEXT7_API_KEYS");
1554 std::env::set_var("CONTEXT7_HOME", &dir_xdg_sem_config);
1555 }
1556 let cwd_original = std::env::current_dir().expect("Deve obter CWD");
1557 std::env::set_current_dir(&dir_cwd).expect("Deve mudar CWD");
1558
1559 let resultado = carregar_chaves_api();
1560
1561 std::env::set_current_dir(&cwd_original).expect("Deve restaurar CWD");
1562 unsafe {
1563 std::env::remove_var("CONTEXT7_HOME");
1564 }
1565
1566 let chaves = resultado.expect("Deve carregar chaves via .env CWD");
1567 assert_eq!(chaves.len(), 1);
1568 assert_eq!(chaves[0], "ctx7sk-cwd-camada-3");
1569 }
1570
1571 #[test]
1572 #[serial_test::serial]
1573 fn testa_carregar_chaves_api_faz_fallback_quando_xdg_invalido() {
1574 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1575 let dir_context7 = dir_temp.path().join("context7");
1576 std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
1577
1578 std::fs::write(dir_context7.join("config.toml"), "[[[invalido")
1579 .expect("Deve escrever TOML inválido");
1580
1581 let dir_cwd = dir_temp.path().join("cwd_fallback");
1582 std::fs::create_dir_all(&dir_cwd).expect("Deve criar CWD com .env");
1583 std::fs::write(dir_cwd.join(".env"), "CONTEXT7_API=ctx7sk-fallback-cwd\n")
1584 .expect("Deve escrever .env no CWD");
1585
1586 unsafe {
1588 std::env::remove_var("CONTEXT7_API_KEYS");
1589 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1590 }
1591 let cwd_original = std::env::current_dir().expect("Deve obter CWD");
1592 std::env::set_current_dir(&dir_cwd).expect("Deve mudar CWD");
1593
1594 let resultado = carregar_chaves_api();
1595
1596 std::env::set_current_dir(&cwd_original).expect("Deve restaurar CWD");
1597 unsafe {
1598 std::env::remove_var("CONTEXT7_HOME");
1599 }
1600
1601 let chaves = resultado.expect("Deve carregar chaves via fallback .env CWD");
1602 assert_eq!(chaves.len(), 1);
1603 assert_eq!(chaves[0], "ctx7sk-fallback-cwd");
1604 }
1605
1606 #[test]
1609 #[serial_test::serial]
1610 fn testa_cmd_keys_add_cria_config_quando_nao_existe() {
1611 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1612 unsafe {
1614 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1615 }
1616
1617 let resultado = cmd_keys_add("ctx7sk-nova-chave-add-test");
1618
1619 unsafe {
1620 std::env::remove_var("CONTEXT7_HOME");
1621 }
1622
1623 resultado.expect("cmd_keys_add deve funcionar em config vazio");
1624
1625 let caminho = dir_temp.path().join("context7").join("config.toml");
1626 assert!(
1627 caminho.exists(),
1628 "config.toml deve existir após cmd_keys_add"
1629 );
1630
1631 let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config criado");
1632 assert_eq!(config.keys.len(), 1, "Config deve ter 1 chave");
1633 assert_eq!(config.keys[0].value, "ctx7sk-nova-chave-add-test");
1634 }
1635
1636 #[test]
1637 #[serial_test::serial]
1638 fn testa_cmd_keys_add_acumula_em_config_existente() {
1639 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1640 unsafe {
1642 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1643 }
1644
1645 cmd_keys_add("ctx7sk-chave-um").expect("Primeira adição deve funcionar");
1646 cmd_keys_add("ctx7sk-chave-dois").expect("Segunda adição deve funcionar");
1647
1648 let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1649 let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config");
1650
1651 unsafe {
1652 std::env::remove_var("CONTEXT7_HOME");
1653 }
1654
1655 assert_eq!(config.keys.len(), 2, "Deve acumular 2 chaves");
1656 assert_eq!(config.keys[0].value, "ctx7sk-chave-um");
1657 assert_eq!(config.keys[1].value, "ctx7sk-chave-dois");
1658 }
1659
1660 #[test]
1661 #[serial_test::serial]
1662 fn testa_cmd_keys_add_nao_duplica_chave_existente() {
1663 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1664 unsafe {
1666 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1667 }
1668
1669 cmd_keys_add("ctx7sk-unica-dedup").expect("Primeira adição deve funcionar");
1670 cmd_keys_add("ctx7sk-unica-dedup").expect("Segunda adição da mesma chave não deve falhar");
1671
1672 let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1673 let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config");
1674
1675 unsafe {
1676 std::env::remove_var("CONTEXT7_HOME");
1677 }
1678
1679 assert_eq!(config.keys.len(), 1, "Não deve duplicar chave já existente");
1680 }
1681
1682 #[test]
1683 #[cfg(unix)]
1684 #[serial_test::serial]
1685 fn testa_cmd_keys_add_aplica_permissoes_600_em_unix() {
1686 use std::os::unix::fs::PermissionsExt;
1687
1688 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1689 unsafe {
1691 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1692 }
1693
1694 cmd_keys_add("ctx7sk-perm-600-keys-add").expect("Deve adicionar chave sem erro");
1695
1696 let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1697 unsafe {
1698 std::env::remove_var("CONTEXT7_HOME");
1699 }
1700
1701 let metadados = std::fs::metadata(&caminho).expect("Deve obter metadados");
1702 let modo = metadados.permissions().mode() & 0o777;
1703 assert_eq!(
1704 modo, 0o600,
1705 "Permissões devem ser 600 após cmd_keys_add, obteve: {:o}",
1706 modo
1707 );
1708 }
1709
1710 #[test]
1713 #[serial_test::serial]
1714 fn testa_cmd_keys_remove_indice_1_de_config_com_3_chaves() {
1715 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1716 unsafe {
1718 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1719 }
1720
1721 escrever_config_xdg("ctx7sk-rem-alpha").expect("Deve escrever chave 1");
1722 escrever_config_xdg("ctx7sk-rem-beta").expect("Deve escrever chave 2");
1723 escrever_config_xdg("ctx7sk-rem-gamma").expect("Deve escrever chave 3");
1724
1725 cmd_keys_remove(1).expect("Remove índice 1 deve funcionar");
1726
1727 let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1728 let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config");
1729
1730 unsafe {
1731 std::env::remove_var("CONTEXT7_HOME");
1732 }
1733
1734 assert_eq!(config.keys.len(), 2, "Devem sobrar 2 chaves após remoção");
1735 assert_eq!(config.keys[0].value, "ctx7sk-rem-beta");
1736 assert_eq!(config.keys[1].value, "ctx7sk-rem-gamma");
1737 }
1738
1739 #[test]
1740 #[serial_test::serial]
1741 fn testa_cmd_keys_remove_indice_2_de_config_com_3_chaves() {
1742 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1743 unsafe {
1745 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1746 }
1747
1748 escrever_config_xdg("ctx7sk-mid-alpha").expect("Deve escrever chave 1");
1749 escrever_config_xdg("ctx7sk-mid-beta").expect("Deve escrever chave 2");
1750 escrever_config_xdg("ctx7sk-mid-gamma").expect("Deve escrever chave 3");
1751
1752 cmd_keys_remove(2).expect("Remove índice 2 deve funcionar");
1753
1754 let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1755 let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config");
1756
1757 unsafe {
1758 std::env::remove_var("CONTEXT7_HOME");
1759 }
1760
1761 assert_eq!(
1762 config.keys.len(),
1763 2,
1764 "Devem sobrar 2 chaves após remoção da do meio"
1765 );
1766 assert_eq!(config.keys[0].value, "ctx7sk-mid-alpha");
1767 assert_eq!(config.keys[1].value, "ctx7sk-mid-gamma");
1768 }
1769
1770 #[test]
1771 #[serial_test::serial]
1772 fn testa_cmd_keys_remove_indice_zero_retorna_err_com_mensagem() {
1773 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1774 unsafe {
1776 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1777 }
1778
1779 escrever_config_xdg("ctx7sk-idx-zero-test").expect("Deve escrever chave");
1780
1781 let resultado = cmd_keys_remove(0);
1782
1783 unsafe {
1784 std::env::remove_var("CONTEXT7_HOME");
1785 }
1786
1787 assert!(
1788 resultado.is_err(),
1789 "Índice 0 inválido deve retornar Err (exit code 1), obteve: {:?}",
1790 resultado
1791 );
1792 }
1793
1794 #[test]
1795 #[serial_test::serial]
1796 fn testa_cmd_keys_remove_indice_maior_que_len_retorna_err_com_mensagem() {
1797 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1798 unsafe {
1800 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1801 }
1802
1803 escrever_config_xdg("ctx7sk-overflow-test").expect("Deve escrever chave");
1804
1805 let resultado = cmd_keys_remove(99);
1806
1807 unsafe {
1808 std::env::remove_var("CONTEXT7_HOME");
1809 }
1810
1811 assert!(
1812 resultado.is_err(),
1813 "Índice fora do range deve retornar Err (exit code 1), obteve: {:?}",
1814 resultado
1815 );
1816 }
1817
1818 #[test]
1819 #[serial_test::serial]
1820 fn testa_cmd_keys_remove_em_config_vazio_retorna_err() {
1821 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1822 unsafe {
1824 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1825 }
1826
1827 let resultado = cmd_keys_remove(1);
1828
1829 unsafe {
1830 std::env::remove_var("CONTEXT7_HOME");
1831 }
1832
1833 assert!(
1834 resultado.is_err(),
1835 "Remover de config vazio deve retornar Err (exit code 1), obteve: {:?}",
1836 resultado
1837 );
1838 }
1839
1840 #[test]
1843 #[serial_test::serial]
1844 fn testa_cmd_keys_clear_com_yes_true_limpa_todas_as_chaves() {
1845 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1846 unsafe {
1848 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1849 }
1850
1851 escrever_config_xdg("ctx7sk-clear-alpha").expect("Deve escrever chave 1");
1852 escrever_config_xdg("ctx7sk-clear-beta").expect("Deve escrever chave 2");
1853
1854 let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1855 let antes = ler_config_toml_do_caminho(&caminho).expect("Deve ler config antes");
1856 assert_eq!(antes.keys.len(), 2, "Pré-condição: 2 chaves antes do clear");
1857
1858 cmd_keys_clear(true).expect("clear com yes=true deve funcionar");
1859
1860 let depois = ler_config_toml_do_caminho(&caminho).expect("Deve ler config depois");
1861
1862 unsafe {
1863 std::env::remove_var("CONTEXT7_HOME");
1864 }
1865
1866 assert!(
1867 depois.keys.is_empty(),
1868 "Após clear com yes=true, chaves devem estar vazias"
1869 );
1870 }
1871
1872 #[test]
1873 #[serial_test::serial]
1874 fn testa_cmd_keys_clear_com_yes_true_em_config_inexistente_funciona() {
1875 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1876 unsafe {
1878 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1879 }
1880
1881 let resultado = cmd_keys_clear(true);
1882
1883 unsafe {
1884 std::env::remove_var("CONTEXT7_HOME");
1885 }
1886
1887 assert!(
1888 resultado.is_ok(),
1889 "clear em config inexistente deve retornar Ok (idempotente), obteve: {:?}",
1890 resultado
1891 );
1892 }
1893
1894 #[test]
1897 #[serial_test::serial]
1898 fn testa_cmd_keys_import_env_valido_com_multiplas_chaves() {
1899 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1900 let arquivo_env = dir_temp.path().join("chaves.env");
1901 std::fs::write(
1902 &arquivo_env,
1903 "CONTEXT7_API=ctx7sk-import-alpha\nCONTEXT7_API=ctx7sk-import-beta\n",
1904 )
1905 .expect("Deve escrever .env de teste");
1906
1907 unsafe {
1909 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1910 }
1911
1912 let resultado = cmd_keys_import(&arquivo_env);
1913
1914 let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1915 let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config após import");
1916
1917 unsafe {
1918 std::env::remove_var("CONTEXT7_HOME");
1919 }
1920
1921 resultado.expect("import de .env válido deve funcionar");
1922 assert_eq!(config.keys.len(), 2, "Deve ter importado 2 chaves");
1923
1924 let valores: Vec<&str> = config.keys.iter().map(|c| c.value.as_str()).collect();
1925 assert!(valores.contains(&"ctx7sk-import-alpha"));
1926 assert!(valores.contains(&"ctx7sk-import-beta"));
1927 }
1928
1929 #[test]
1930 #[serial_test::serial]
1931 fn testa_cmd_keys_import_env_sem_chaves_retorna_err() {
1932 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1933 let arquivo_env = dir_temp.path().join("vazio.env");
1934 std::fs::write(&arquivo_env, "# apenas comentario\nOUTRA_VAR=valor\n")
1935 .expect("Deve escrever .env sem chaves");
1936
1937 unsafe {
1939 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1940 }
1941
1942 let resultado = cmd_keys_import(&arquivo_env);
1943
1944 unsafe {
1945 std::env::remove_var("CONTEXT7_HOME");
1946 }
1947
1948 assert!(
1949 resultado.is_err(),
1950 "Import de .env sem chaves CONTEXT7_API deve retornar Err"
1951 );
1952 }
1953
1954 #[test]
1955 #[serial_test::serial]
1956 fn testa_cmd_keys_import_arquivo_inexistente_retorna_err() {
1957 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1958 let arquivo_inexistente = dir_temp.path().join("nao_existe.env");
1959
1960 unsafe {
1962 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1963 }
1964
1965 let resultado = cmd_keys_import(&arquivo_inexistente);
1966
1967 unsafe {
1968 std::env::remove_var("CONTEXT7_HOME");
1969 }
1970
1971 assert!(
1972 resultado.is_err(),
1973 "Import de arquivo inexistente deve retornar Err"
1974 );
1975 }
1976
1977 #[test]
1978 #[serial_test::serial]
1979 fn testa_cmd_keys_import_roundtrip_add_depois_list() {
1980 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1981 let arquivo_env = dir_temp.path().join("roundtrip.env");
1982 std::fs::write(
1983 &arquivo_env,
1984 "CONTEXT7_API=ctx7sk-rtrip-01\nCONTEXT7_API=ctx7sk-rtrip-02\n",
1985 )
1986 .expect("Deve escrever .env de roundtrip");
1987
1988 unsafe {
1990 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1991 }
1992
1993 cmd_keys_import(&arquivo_env).expect("Import deve funcionar");
1994
1995 let config = ler_config_xdg_raw()
1996 .expect("Deve retornar Ok")
1997 .expect("Deve retornar Some após import");
1998
1999 unsafe {
2000 std::env::remove_var("CONTEXT7_HOME");
2001 }
2002
2003 assert_eq!(
2004 config.keys.len(),
2005 2,
2006 "Roundtrip: deve ter 2 chaves após import"
2007 );
2008 assert_eq!(config.keys[0].value, "ctx7sk-rtrip-01");
2009 assert_eq!(config.keys[1].value, "ctx7sk-rtrip-02");
2010 }
2011
2012 #[test]
2015 #[serial_test::serial]
2016 fn testa_cmd_keys_export_em_config_vazio_retorna_ok() {
2017 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
2018 unsafe {
2020 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
2021 }
2022
2023 let resultado = cmd_keys_export();
2024
2025 unsafe {
2026 std::env::remove_var("CONTEXT7_HOME");
2027 }
2028
2029 assert!(
2030 resultado.is_ok(),
2031 "Export de config vazio deve retornar Ok, obteve: {:?}",
2032 resultado
2033 );
2034 }
2035
2036 #[test]
2037 #[serial_test::serial]
2038 fn testa_cmd_keys_export_retorna_ok_com_chaves_existentes() {
2039 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
2040 unsafe {
2042 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
2043 }
2044
2045 escrever_config_xdg("ctx7sk-export-um").expect("Deve escrever chave 1");
2046 escrever_config_xdg("ctx7sk-export-dois").expect("Deve escrever chave 2");
2047
2048 let resultado = cmd_keys_export();
2049
2050 unsafe {
2051 std::env::remove_var("CONTEXT7_HOME");
2052 }
2053
2054 assert!(
2055 resultado.is_ok(),
2056 "Export com chaves existentes deve retornar Ok, obteve: {:?}",
2057 resultado
2058 );
2059 }
2060
2061 #[test]
2064 #[serial_test::serial]
2065 fn testa_ler_config_xdg_raw_retorna_none_sem_arquivo() {
2066 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
2067 unsafe {
2069 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
2070 }
2071
2072 let resultado = ler_config_xdg_raw();
2073
2074 unsafe {
2075 std::env::remove_var("CONTEXT7_HOME");
2076 }
2077
2078 let valor = resultado.expect("Deve retornar Ok");
2079 assert!(
2080 valor.is_none(),
2081 "Deve retornar None quando config.toml não existe"
2082 );
2083 }
2084
2085 #[test]
2086 #[serial_test::serial]
2087 fn testa_ler_config_xdg_raw_retorna_config_com_chaves() {
2088 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
2089 let dir_context7 = dir_temp.path().join("context7");
2090 std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
2091
2092 let toml = r#"schema_version = 1
2093
2094[[keys]]
2095value = "ctx7sk-raw-01"
2096added_at = "2026-04-08T00:00:00+00:00"
2097
2098[[keys]]
2099value = "ctx7sk-raw-02"
2100added_at = "2026-04-08T00:01:00+00:00"
2101"#;
2102 std::fs::write(dir_context7.join("config.toml"), toml).expect("Deve escrever config.toml");
2103
2104 unsafe {
2106 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
2107 }
2108
2109 let resultado = ler_config_xdg_raw();
2110
2111 unsafe {
2112 std::env::remove_var("CONTEXT7_HOME");
2113 }
2114
2115 let config = resultado
2116 .expect("Deve retornar Ok")
2117 .expect("Deve retornar Some com config");
2118 assert_eq!(config.keys.len(), 2);
2119 assert_eq!(config.keys[0].value, "ctx7sk-raw-01");
2120 assert_eq!(config.keys[1].value, "ctx7sk-raw-02");
2121 }
2122
2123 #[test]
2124 #[serial_test::serial]
2125 fn testa_cmd_keys_path_retorna_ok() {
2126 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
2127 unsafe {
2129 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
2130 }
2131
2132 let resultado = cmd_keys_path();
2133
2134 unsafe {
2135 std::env::remove_var("CONTEXT7_HOME");
2136 }
2137
2138 resultado.expect("cmd_keys_path deve retornar Ok");
2139 }
2140
2141 #[test]
2142 #[serial_test::serial]
2143 fn testa_descobrir_caminho_config_termina_com_config_toml() {
2144 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
2145 unsafe {
2147 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
2148 }
2149
2150 let caminho = descobrir_caminho_config();
2151
2152 unsafe {
2153 std::env::remove_var("CONTEXT7_HOME");
2154 }
2155
2156 let caminho = caminho.expect("Deve retornar caminho XDG válido");
2157 assert!(
2158 caminho.to_string_lossy().ends_with("config.toml"),
2159 "Caminho deve terminar com config.toml, obteve: {}",
2160 caminho.display()
2161 );
2162 assert!(
2163 caminho.to_string_lossy().contains("context7"),
2164 "Caminho deve conter 'context7', obteve: {}",
2165 caminho.display()
2166 );
2167 }
2168
2169 #[test]
2172 #[serial_test::serial]
2173 fn testa_fluxo_completo_add_list_remove_clear() {
2174 let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
2175 unsafe {
2177 std::env::set_var("CONTEXT7_HOME", dir_temp.path());
2178 }
2179
2180 cmd_keys_add("ctx7sk-fluxo-01").expect("Add 1 deve funcionar");
2181 cmd_keys_add("ctx7sk-fluxo-02").expect("Add 2 deve funcionar");
2182 cmd_keys_add("ctx7sk-fluxo-03").expect("Add 3 deve funcionar");
2183
2184 let config_antes = ler_config_xdg_raw()
2185 .expect("Ok")
2186 .expect("Some com 3 chaves");
2187 assert_eq!(config_antes.keys.len(), 3, "Deve ter 3 chaves após 3 adds");
2188
2189 cmd_keys_remove(2).expect("Remove índice 2 deve funcionar");
2190
2191 let config_pos_remove = ler_config_xdg_raw()
2192 .expect("Ok")
2193 .expect("Some com 2 chaves");
2194 assert_eq!(
2195 config_pos_remove.keys.len(),
2196 2,
2197 "Deve ter 2 chaves após remove"
2198 );
2199 assert_eq!(config_pos_remove.keys[0].value, "ctx7sk-fluxo-01");
2200 assert_eq!(config_pos_remove.keys[1].value, "ctx7sk-fluxo-03");
2201
2202 cmd_keys_clear(true).expect("Clear com yes=true deve funcionar");
2203
2204 let caminho = descobrir_caminho_config().expect("Deve ter caminho");
2205 let config_final = ler_config_toml_do_caminho(&caminho).expect("Deve ler config final");
2206
2207 unsafe {
2208 std::env::remove_var("CONTEXT7_HOME");
2209 }
2210
2211 assert!(
2212 config_final.keys.is_empty(),
2213 "Após clear, chaves devem estar vazias"
2214 );
2215 }
2216
2217 #[test]
2220 #[serial_test::serial]
2221 fn testa_context7_home_override_config_path() {
2222 let tmp = tempfile::TempDir::new().expect("Deve criar tempdir");
2223 unsafe {
2226 std::env::set_var("CONTEXT7_HOME", tmp.path());
2227 }
2228
2229 let caminho = descobrir_caminho_config();
2230
2231 unsafe {
2232 std::env::remove_var("CONTEXT7_HOME");
2233 }
2234
2235 let caminho = caminho.expect("Deve retornar Some quando CONTEXT7_HOME está definido");
2236 let esperado = tmp.path().join("context7").join("config.toml");
2237 assert_eq!(
2238 caminho, esperado,
2239 "CONTEXT7_HOME deve definir caminho como {{CONTEXT7_HOME}}/context7/config.toml"
2240 );
2241 }
2242
2243 #[test]
2244 #[serial_test::serial]
2245 fn testa_context7_home_override_logs_path() {
2246 let tmp = tempfile::TempDir::new().expect("Deve criar tempdir");
2247 unsafe {
2249 std::env::set_var("CONTEXT7_HOME", tmp.path());
2250 }
2251
2252 let caminho = descobrir_caminho_logs_xdg();
2253
2254 unsafe {
2255 std::env::remove_var("CONTEXT7_HOME");
2256 }
2257
2258 let caminho = caminho.expect("Deve retornar Some quando CONTEXT7_HOME está definido");
2259 let esperado = tmp.path().join("context7").join("logs");
2260 assert_eq!(
2261 caminho, esperado,
2262 "CONTEXT7_HOME deve definir logs como {{CONTEXT7_HOME}}/context7/logs"
2263 );
2264 }
2265
2266 #[test]
2267 #[serial_test::serial]
2268 fn testa_context7_home_vazio_cai_em_projectdirs() {
2269 let tmp = tempfile::TempDir::new().expect("Deve criar tempdir");
2270 unsafe {
2272 std::env::set_var("CONTEXT7_HOME", "");
2273 }
2274
2275 let caminho = descobrir_caminho_config();
2276
2277 unsafe {
2278 std::env::remove_var("CONTEXT7_HOME");
2279 }
2280
2281 if let Some(c) = caminho {
2283 let tmp_str = tmp.path().to_string_lossy();
2284 assert!(
2285 !c.to_string_lossy().starts_with(tmp_str.as_ref()),
2286 "CONTEXT7_HOME vazio não deve usar o tempdir: {}",
2287 c.display()
2288 );
2289 }
2290 }
2292}