Skip to main content

context7_cli/
storage.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! XDG storage backend for API keys (four-layer hierarchy).
3//!
4//! Implements the four-layer key loading hierarchy:
5//! 1. `CONTEXT7_API_KEYS` runtime environment variable (highest priority)
6//! 2. XDG config file `~/.config/context7/config.toml`
7//! 3. `.env` file in the current working directory
8//! 4. `CONTEXT7_API_KEYS` compile-time environment variable (lowest priority)
9use anyhow::{bail, Context, Result};
10use chrono::Utc;
11use directories::ProjectDirs;
12use serde::{Deserialize, Serialize};
13use std::path::PathBuf;
14
15use zeroize::{Zeroize, ZeroizeOnDrop};
16
17use unicode_normalization::UnicodeNormalization;
18
19use crate::errors::Context7Error;
20use crate::i18n::{t, Message};
21
22// ─── XDG CONFIG STRUCTS ────────────────────────────────────────────
23
24/// Represents a stored API key entry in the XDG configuration file.
25///
26/// Field names use English (`value`, `added_at`) to mirror the external TOML format.
27#[derive(Debug, Serialize, Deserialize, Clone)]
28pub struct StoredKey {
29    /// The API key value.
30    pub value: String,
31    /// RFC 3339 timestamp when the key was added.
32    pub added_at: String,
33}
34
35/// Represents the structured TOML configuration file.
36///
37/// Field names use English (`schema_version`, `keys`) to mirror the external TOML format.
38#[derive(Debug, Serialize, Deserialize, Default)]
39pub struct FileConfig {
40    /// Configuration schema version (currently 1).
41    pub schema_version: u32,
42    /// List of stored API keys.
43    #[serde(default)]
44    pub keys: Vec<StoredKey>,
45}
46
47// ─── SECURE NEWTYPE FOR API KEYS ─────────────────────────────────────────
48
49/// Secure wrapper for API keys with automatic memory cleanup.
50///
51/// Implements `Zeroize` and `ZeroizeOnDrop` to ensure keys are
52/// removed from memory when leaving scope. `Debug` and `Display` show
53/// only the masked version of the key to prevent leakage in logs.
54#[derive(Clone, Zeroize, ZeroizeOnDrop)]
55pub struct ApiKey(String);
56
57impl ApiKey {
58    /// Creates a new instance from a key string.
59    pub fn new(value: String) -> Self {
60        Self(value)
61    }
62
63    /// Returns a reference to the inner key value.
64    pub fn value(&self) -> &str {
65        &self.0
66    }
67}
68
69impl PartialEq<&str> for ApiKey {
70    fn eq(&self, other: &&str) -> bool {
71        self.0 == *other
72    }
73}
74
75impl std::fmt::Debug for ApiKey {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        write!(f, "ApiKey({})", mask_key(self.value()))
78    }
79}
80
81impl std::fmt::Display for ApiKey {
82    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83        write!(f, "{}", mask_key(self.value()))
84    }
85}
86
87// ─── FILE PERMISSIONS FUNCTIONS ───────────────────────────────────────
88
89/// Sets 600 permissions (owner read/write only) on Unix systems.
90///
91/// On non-Unix systems this is a no-op. Centralises the chmod 600 used by
92/// [`write_xdg_config`] and [`write_file_config`].
93#[must_use]
94pub fn apply_600_permissions(path: &std::path::Path) -> Result<()> {
95    #[cfg(unix)]
96    {
97        use std::os::unix::fs::PermissionsExt;
98        let mut perms = std::fs::metadata(path)
99            .with_context(|| format!("Failed to read metadata of: {}", path.display()))?
100            .permissions();
101        perms.set_mode(0o600);
102        std::fs::set_permissions(path, perms)
103            .with_context(|| format!("Failed to set permissions on: {}", path.display()))?;
104    }
105    #[cfg(not(unix))]
106    let _ = path;
107    Ok(())
108}
109
110// ─── XDG PATH DISCOVERY FUNCTIONS ──────────────────────────────────
111
112/// Resolves a base path override from the `CONTEXT7_HOME` environment variable.
113///
114/// Returns `Some(PathBuf)` if `CONTEXT7_HOME` is set, non-empty, and contains no
115/// path-traversal components (`..`). Returns `None` otherwise, allowing callers
116/// to fall back to XDG/ProjectDirs defaults.
117fn resolve_home_override() -> Option<PathBuf> {
118    let home = std::env::var("CONTEXT7_HOME").ok()?;
119    if home.is_empty() {
120        return None;
121    }
122    let base = PathBuf::from(&home);
123    // Reject path traversal to prevent escaping the configuration directory
124    if base
125        .components()
126        .any(|c| c == std::path::Component::ParentDir)
127    {
128        tracing::warn!(
129            "CONTEXT7_HOME='{}' rejected (path traversal) — using XDG default",
130            home
131        );
132        return None;
133    }
134    // Reject Windows reserved names in any path component
135    let reserved_names = [
136        "CON", "PRN", "AUX", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7",
137        "COM8", "COM9", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8",
138        "LPT9",
139    ];
140    for component in base.components() {
141        if let std::path::Component::Normal(name) = component {
142            let name_upper = name.to_string_lossy().to_uppercase();
143            // Check pure name and name with extension (e.g. NUL.txt)
144            let base_name = name_upper.split('.').next().unwrap_or("");
145            if reserved_names.contains(&base_name) {
146                tracing::warn!(
147                    "CONTEXT7_HOME='{}' rejected (Windows reserved name '{}') — using XDG default",
148                    home, base_name
149                );
150                return None;
151            }
152        }
153    }
154    Some(base)
155}
156
157/// Discovers the XDG configuration path for the `config.toml` file.
158///
159/// Checks `CONTEXT7_HOME` first: if set and valid, returns
160/// `{CONTEXT7_HOME}/context7/config.toml`. Falls back to
161/// `ProjectDirs::from("", "", "context7")` → `config_dir()`.
162/// Returns `None` if neither source provides a path.
163#[must_use]
164pub fn discover_config_path() -> Option<PathBuf> {
165    let path = if let Some(base) = resolve_home_override() {
166        base.join("context7").join("config.toml")
167    } else {
168        ProjectDirs::from("", "", "context7")?
169            .config_dir()
170            .join("config.toml")
171    };
172    // Normalise to NFC — macOS HFS+ uses NFD by default
173    let path_str = path.to_string_lossy().nfc().collect::<String>();
174    Some(PathBuf::from(path_str))
175}
176
177/// Discovers the XDG path for storing log files.
178///
179/// Checks `CONTEXT7_HOME` first: if set and valid, returns
180/// `{CONTEXT7_HOME}/context7/logs`. Falls back to `state_dir()` on Linux
181/// (XDG_STATE_HOME) with fallback to `data_local_dir()`.
182/// Returns `None` if neither source provides a path.
183#[must_use]
184pub fn discover_xdg_log_paths() -> Option<PathBuf> {
185    let path = if let Some(base) = resolve_home_override() {
186        base.join("context7").join("logs")
187    } else {
188        let dirs = ProjectDirs::from("", "", "context7")?;
189        // state_dir() is only available on Linux/XDG; cross-platform fallback
190        #[cfg(target_os = "linux")]
191        {
192            dirs.state_dir()
193                .unwrap_or_else(|| dirs.data_local_dir())
194                .to_path_buf()
195        }
196        #[cfg(not(target_os = "linux"))]
197        {
198            dirs.data_local_dir().to_path_buf()
199        }
200    };
201    // Normalise to NFC — macOS HFS+ uses NFD by default
202    let path_str = path.to_string_lossy().nfc().collect::<String>();
203    Some(PathBuf::from(path_str))
204}
205
206// ─── KEY LOADING FUNCTIONS (HIERARCHY) ─────────────────────────
207
208/// Layer 1: reads keys from the `CONTEXT7_API_KEYS` runtime environment variable.
209///
210/// Accepts multiple comma-separated keys:
211/// `CONTEXT7_API_KEYS=ctx7sk-a,ctx7sk-b,ctx7sk-c`
212/// Whitespace around each key is trimmed automatically.
213/// Returns `None` if the variable is not set or is empty.
214#[must_use]
215pub fn read_env_var_key() -> Option<Vec<String>> {
216    std::env::var("CONTEXT7_API_KEYS")
217        .ok()
218        .map(|value| {
219            let estimate = value.matches(',').count() + 1;
220            let mut keys = Vec::with_capacity(estimate);
221            for s in value.split(',') {
222                let trimmed = s.trim().to_string();
223                if !trimmed.is_empty() {
224                    keys.push(trimmed);
225                }
226            }
227            keys
228        })
229        .filter(|v| !v.is_empty())
230}
231
232/// Layer 2: reads keys from the XDG configuration file (`config.toml`).
233///
234/// Returns `None` if the file does not exist or the XDG path is unavailable.
235/// Returns `Err` if the file exists but contains invalid TOML.
236#[must_use]
237pub fn read_xdg_config() -> Result<Option<Vec<String>>> {
238    let path = match discover_config_path() {
239        Some(p) => p,
240        None => return Ok(None),
241    };
242
243    if !path.exists() {
244        return Ok(None);
245    }
246
247    let content = std::fs::read_to_string(&path)
248        .with_context(|| format!("Failed to read XDG configuration at: {}", path.display()))?;
249
250    let config: FileConfig = toml::from_str(&content)
251        .with_context(|| format!("Invalid TOML at: {}", path.display()))?;
252
253    let keys: Vec<String> = config
254        .keys
255        .into_iter()
256        .map(|c| c.value)
257        .filter(|v| !v.is_empty())
258        .collect();
259
260    if keys.is_empty() {
261        Ok(None)
262    } else {
263        Ok(Some(keys))
264    }
265}
266
267/// Layer 3: reads keys from the `.env` file in the current working directory.
268///
269/// Re-uses [`extract_env_keys`] which is pure and testable.
270/// Returns `None` if the `.env` file does not exist or has no valid keys.
271#[must_use]
272pub fn read_env_cwd() -> Option<Vec<String>> {
273    let path = std::env::current_dir().ok().map(|d| d.join(".env"))?;
274
275    if !path.exists() {
276        return None;
277    }
278
279    std::fs::read_to_string(&path)
280        .ok()
281        .and_then(|content| extract_env_keys(&content).ok())
282}
283
284/// Layer 4: reads keys embedded at compile time via `option_env!("CONTEXT7_API_KEYS")`.
285///
286/// Allows embedding keys in the binary at build time:
287/// `CONTEXT7_API_KEYS=ctx7sk-a cargo build --release`
288///
289/// **Security warning**: compile-time keys are visible to anyone who inspects
290/// the binary (e.g. `strings context7 | grep ctx7sk-`). Use only in controlled
291/// pipelines where access to the binary artefact is restricted.
292///
293/// Returns `None` if the variable was not defined at compile time.
294#[must_use]
295pub fn read_compile_time_env() -> Option<Vec<String>> {
296    option_env!("CONTEXT7_API_KEYS").map(|value| {
297        let estimate = value.matches(',').count() + 1;
298        let mut keys = Vec::with_capacity(estimate);
299        for s in value.split(',') {
300            let trimmed = s.trim().to_string();
301            if !trimmed.is_empty() {
302                keys.push(trimmed);
303            }
304        }
305        keys
306    })
307}
308
309/// Loads API keys using the four-layer precedence hierarchy:
310///
311/// 1. `CONTEXT7_API_KEYS` runtime env var (highest priority)
312/// 2. XDG config `~/.config/context7/config.toml`
313/// 3. `.env` file in the current working directory
314/// 4. `CONTEXT7_API_KEYS` compile-time env var (lowest priority)
315///
316/// Returns an error only if NO layer provides valid keys.
317#[must_use]
318pub fn load_api_keys() -> Result<Vec<ApiKey>> {
319    use tracing::{info, warn};
320
321    // Layer 1: runtime env var
322    if let Some(keys) = read_env_var_key() {
323        info!("Keys loaded from CONTEXT7_API_KEYS environment variable");
324        return Ok(keys.into_iter().map(ApiKey::new).collect());
325    }
326
327    // Layer 2: XDG config
328    match read_xdg_config() {
329        Ok(Some(keys)) => {
330            info!("Keys loaded from XDG configuration");
331            return Ok(keys.into_iter().map(ApiKey::new).collect());
332        }
333        Ok(None) => {}
334        Err(e) => {
335            warn!("Failed to read XDG configuration (continuing): {}", e);
336        }
337    }
338
339    // Layer 3: .env in CWD
340    if let Some(keys) = read_env_cwd() {
341        info!(
342            "Starting context7 with {} API keys available",
343            keys.len()
344        );
345        return Ok(keys.into_iter().map(ApiKey::new).collect());
346    }
347
348    // Layer 4: compile-time
349    if let Some(keys) = read_compile_time_env() {
350        info!("Keys loaded from compile-time CONTEXT7_API_KEYS");
351        return Ok(keys.into_iter().map(ApiKey::new).collect());
352    }
353
354    bail!(t(Message::NoKeyConfigured))
355}
356
357// ─── CONFIG WRITE FUNCTIONS ───────────────────────────────────────────
358
359/// Writes (or updates) the XDG configuration file with the provided key.
360///
361/// Creates parent directories if necessary.
362/// On Unix systems, sets 600 permissions via [`apply_600_permissions`].
363#[must_use]
364pub fn write_xdg_config(new_key: &str) -> Result<PathBuf> {
365    let path = discover_config_path()
366        .context("System does not support XDG directories — cannot save configuration")?;
367
368    // Create parent directories if they do not exist
369    if let Some(parent) = path.parent() {
370        std::fs::create_dir_all(parent)
371            .with_context(|| format!("Failed to create directory: {}", parent.display()))?;
372    }
373
374    // Read existing config or create a new one
375    let mut config = if path.exists() {
376        let content = std::fs::read_to_string(&path)
377            .with_context(|| format!("Failed to read existing config: {}", path.display()))?;
378        toml::from_str::<FileConfig>(&content)
379            .with_context(|| format!("Invalid TOML at: {}", path.display()))?
380    } else {
381        FileConfig {
382            schema_version: 1,
383            keys: Vec::new(),
384        }
385    };
386
387    // Add new key if it does not already exist
388    let already_exists = config.keys.iter().any(|c| c.value == new_key);
389    if !already_exists {
390        config.keys.push(StoredKey {
391            value: new_key.to_string(),
392            added_at: Utc::now().to_rfc3339(),
393        });
394    }
395
396    // Serialise and write
397    let toml_str =
398        toml::to_string_pretty(&config).context("Failed to serialise configuration to TOML")?;
399    std::fs::write(&path, &toml_str)
400        .with_context(|| format!("Failed to write config at: {}", path.display()))?;
401
402    apply_600_permissions(&path)?;
403
404    Ok(path)
405}
406
407/// Reads the XDG configuration file and returns the full [`FileConfig`].
408///
409/// Used by operations that need the complete structure (list, remove, export).
410/// Returns `Ok(None)` if the file does not exist or the XDG path is unavailable.
411#[must_use]
412pub fn read_xdg_config_raw() -> Result<Option<FileConfig>> {
413    let path = match discover_config_path() {
414        Some(p) => p,
415        None => return Ok(None),
416    };
417
418    if !path.exists() {
419        return Ok(None);
420    }
421
422    let content = std::fs::read_to_string(&path)
423        .with_context(|| format!("Failed to read XDG configuration at: {}", path.display()))?;
424
425    let config: FileConfig = toml::from_str(&content)
426        .with_context(|| format!("Invalid TOML at: {}", path.display()))?;
427
428    Ok(Some(config))
429}
430
431/// Writes a complete [`FileConfig`] to the XDG configuration file.
432///
433/// Creates parent directories if necessary.
434/// On Unix systems, sets 600 permissions.
435#[must_use]
436pub fn write_file_config(config: &FileConfig) -> Result<PathBuf> {
437    let path = discover_config_path()
438        .context("System does not support XDG directories — cannot save configuration")?;
439
440    if let Some(parent) = path.parent() {
441        std::fs::create_dir_all(parent)
442            .with_context(|| format!("Failed to create directory: {}", parent.display()))?;
443    }
444
445    let toml_str =
446        toml::to_string_pretty(config).context("Failed to serialise configuration to TOML")?;
447    std::fs::write(&path, &toml_str)
448        .with_context(|| format!("Failed to write config at: {}", path.display()))?;
449
450    apply_600_permissions(&path)?;
451
452    Ok(path)
453}
454
455// ─── AUXILIARY FUNCTIONS ─────────────────────────────────────────────────────
456
457/// Masks an API key showing only the first 12 and last 4 characters.
458///
459/// Example: `ctx7sk-abc123...xyz9`
460///
461/// If the key is too short (≤ 16 Unicode characters), returns `***` for protection.
462/// Uses `chars()` for UTF-8 safety — avoids panics from byte-indexing multibyte characters.
463#[must_use]
464pub fn mask_key(key: &str) -> String {
465    let n_chars = key.chars().count();
466    let prefix_len = 12;
467    let suffix_len = 4;
468    if n_chars <= prefix_len + suffix_len {
469        return "***".to_string();
470    }
471    let prefix: String = key.chars().take(prefix_len).collect();
472    let suffix: String = key
473        .chars()
474        .rev()
475        .take(suffix_len)
476        .collect::<String>()
477        .chars()
478        .rev()
479        .collect();
480    format!("{}...{}", prefix, suffix)
481}
482
483/// Extracts `CONTEXT7_API=` keys from `.env` file content in memory.
484///
485/// Ignores comments (lines starting with `#`) and blank lines.
486/// Removes surrounding double and single quotes from values.
487/// Pure function — accepts `&str`, no I/O, facilitates unit testing.
488#[must_use]
489pub fn extract_env_keys(content: &str) -> Result<Vec<String>> {
490    let keys: Vec<String> = content
491        .lines()
492        .filter_map(|line| {
493            // Remove inline comments (everything after #)
494            let line_no_comment = line.split('#').next().unwrap_or("").trim();
495            line_no_comment
496                .strip_prefix("CONTEXT7_API=")
497                .map(|value| {
498                    // Remove single or double quotes around the value
499                    value
500                        .trim()
501                        .trim_matches('"')
502                        .trim_matches('\'')
503                        .to_string()
504                })
505                .filter(|v| !v.is_empty())
506        })
507        .collect();
508
509    if keys.is_empty() {
510        bail!(t(Message::NoContext7KeyInFile));
511    }
512
513    Ok(keys)
514}
515
516// ─── KEYS SUBCOMMAND OPERATIONS ───────────────────────────────────────────
517
518/// Adds a new key to the XDG storage.
519///
520/// If the key already exists, prints a warning and returns without modifying the config.
521/// Re-uses [`write_xdg_config`] which implements deduplication and chmod 600.
522#[must_use]
523pub fn cmd_keys_add(key: &str) -> Result<()> {
524    let trimmed_key = key.trim();
525    if trimmed_key.is_empty() {
526        crate::output::print_invalid_empty_key();
527        bail!(Context7Error::KeysOperationFailed);
528    }
529    if !trimmed_key.starts_with("ctx7sk-") || trimmed_key.len() < 16 {
530        crate::output::print_key_format_warning();
531    }
532    // Check for duplicates before writing — to display a clear warning to the user
533    if let Some(config) = read_xdg_config_raw()? {
534        if config.keys.iter().any(|c| c.value == trimmed_key) {
535            crate::output::print_key_already_existed();
536            return Ok(());
537        }
538    }
539    let path = write_xdg_config(trimmed_key)?;
540    crate::output::print_key_added(&path);
541    Ok(())
542}
543
544/// Lists all stored keys with their 1-based indices and masked values.
545///
546/// When `json` is true, outputs a JSON array with `index`, `masked_key`, and `added_at` fields.
547#[must_use]
548pub fn cmd_keys_list(json: bool) -> Result<()> {
549    match read_xdg_config_raw()? {
550        None => {
551            if json {
552                crate::output::print_empty_json_array();
553            } else {
554                crate::output::print_no_keys();
555            }
556        }
557        Some(config) if config.keys.is_empty() => {
558            if json {
559                crate::output::print_empty_json_array();
560            } else {
561                crate::output::print_no_keys();
562            }
563        }
564        Some(config) => {
565            if json {
566                let mut masked: Vec<serde_json::Value> = Vec::with_capacity(config.keys.len());
567                masked.extend(config.keys.iter().enumerate().map(|(i, k)| {
568                    serde_json::json!({
569                        "index": i + 1,
570                        "masked_key": mask_key(&k.value),
571                        "added_at": crate::output::format_added_at_display(&k.added_at)
572                    })
573                }));
574                crate::output::print_raw_json(
575                    &serde_json::to_string_pretty(&masked).with_context(|| {
576                        crate::i18n::t(crate::i18n::Message::JsonSerialiseFailure)
577                    })?,
578                );
579            } else {
580                crate::output::print_masked_keys(&config.keys, mask_key);
581            }
582        }
583    }
584    Ok(())
585}
586
587/// Removes a key by its 1-based index.
588#[must_use]
589pub fn cmd_keys_remove(index: usize) -> Result<()> {
590    let mut config = match read_xdg_config_raw()? {
591        None => {
592            crate::output::print_no_keys_to_remove();
593            bail!(Context7Error::KeysOperationFailed);
594        }
595        Some(c) if c.keys.is_empty() => {
596            crate::output::print_no_keys_to_remove();
597            bail!(Context7Error::KeysOperationFailed);
598        }
599        Some(c) => c,
600    };
601
602    if index == 0 || index > config.keys.len() {
603        crate::output::print_invalid_index(index, config.keys.len());
604        bail!(Context7Error::KeysOperationFailed);
605    }
606
607    let removed = config.keys.remove(index - 1);
608    write_file_config(&config)?;
609    crate::output::print_key_removed(&mask_key(&removed.value));
610    Ok(())
611}
612
613/// Removes all stored keys. Asks for confirmation unless `--yes` is passed.
614#[must_use]
615pub fn cmd_keys_clear(yes: bool) -> Result<()> {
616    if !yes && !crate::output::confirm_clear()? {
617        crate::output::print_operation_cancelled();
618        return Ok(());
619    }
620
621    let config = FileConfig {
622        schema_version: 1,
623        keys: Vec::new(),
624    };
625    write_file_config(&config)?;
626    crate::output::print_keys_removed();
627    Ok(())
628}
629
630/// Displays the path of the XDG configuration file.
631#[must_use]
632#[allow(clippy::unnecessary_wraps)]
633pub fn cmd_keys_path() -> Result<()> {
634    match discover_config_path() {
635        Some(path) => crate::output::print_config_path(&path),
636        None => crate::output::print_xdg_unsupported(),
637    }
638    Ok(())
639}
640
641/// Imports keys from a `.env` file, reading `CONTEXT7_API=` entries.
642///
643/// Re-uses [`extract_env_keys`] and [`write_xdg_config`] for each key.
644#[must_use]
645pub fn cmd_keys_import(file: &std::path::Path) -> Result<()> {
646    let content = std::fs::read_to_string(file)
647        .with_context(|| format!("Failed to read file: {}", file.display()))?;
648
649    let keys =
650        extract_env_keys(&content).with_context(|| format!("File: {}", file.display()))?;
651
652    let total = keys.len();
653    let mut imported = 0usize;
654
655    for key in &keys {
656        write_xdg_config(key)?;
657        imported += 1;
658    }
659
660    crate::output::print_import_completed(imported, total);
661    Ok(())
662}
663
664/// Exports all keys to stdout in `CONTEXT7_API=<value>` format, one per line.
665///
666/// Compatible with `.env` files — useful for scripts and pipes.
667#[must_use]
668pub fn cmd_keys_export() -> Result<()> {
669    match read_xdg_config_raw()? {
670        None => {}
671        Some(config) if config.keys.is_empty() => {}
672        Some(config) => {
673            for key in &config.keys {
674                crate::output::print_exported_key(&key.value);
675            }
676        }
677    }
678    Ok(())
679}
680
681// ─── TESTS ─────────────────────────────────────────────────────────────────
682
683#[cfg(test)]
684mod tests {
685    use super::*;
686
687    // ── Test helper function ──────────────────────────────────────────────
688
689    /// Reads the content of a TOML file from the path and returns `FileConfig`.
690    fn read_toml_config_from_path(path: &std::path::Path) -> Result<FileConfig> {
691        let content = std::fs::read_to_string(path)
692            .with_context(|| format!("Failed to read: {}", path.display()))?;
693        toml::from_str(&content)
694            .with_context(|| format!("Invalid TOML at: {}", path.display()))
695    }
696
697    // ── Parsing do .env ───────────────────────────────────────────────────────
698
699    #[test]
700    fn test_env_parsing_with_multiple_equal_keys() {
701        let mut content = String::new();
702        for i in 0..17 {
703            content.push_str(&format!("CONTEXT7_API=ctx7sk-key-{:02}\n", i));
704        }
705        let keys = extract_env_keys(&content).expect("Must extract 17 keys without error");
706        assert_eq!(keys.len(), 17, "Must return exactly 17 keys");
707        for (i, key) in keys.iter().enumerate() {
708            assert_eq!(
709                key,
710                &format!("ctx7sk-key-{:02}", i),
711                "Chave {} deve ter o value correto",
712                i
713            );
714        }
715    }
716
717    #[test]
718    fn test_env_parsing_ignores_comments_and_blank_lines() {
719        let content = "# Este é um comentário\n\
720                        CONTEXT7_API=ctx7sk-key-valida-01\n\
721                        \n\
722                        # Outro comentário\n\
723                        CONTEXT7_API=ctx7sk-key-valida-02\n\
724                        \n";
725        let keys = extract_env_keys(content).expect("Must extract keys without error");
726        assert_eq!(keys.len(), 2, "Must ignore comments and blank lines");
727        assert_eq!(keys[0], "ctx7sk-key-valida-01");
728        assert_eq!(keys[1], "ctx7sk-key-valida-02");
729    }
730
731    #[test]
732    fn test_env_parsing_removes_double_quotes() {
733        let content = "CONTEXT7_API=\"ctx7sk-abc-com-aspas\"\n";
734        let keys = extract_env_keys(content).expect("Must extract key without error");
735        assert_eq!(keys.len(), 1);
736        assert_eq!(
737            keys[0], "ctx7sk-abc-com-aspas",
738            "Must remove double quotes"
739        );
740    }
741
742    #[test]
743    fn test_env_parsing_removes_single_quotes() {
744        let content = "CONTEXT7_API='ctx7sk-abc-aspas-simples'\n";
745        let keys = extract_env_keys(content).expect("Must extract key without error");
746        assert_eq!(keys.len(), 1);
747        assert_eq!(
748            keys[0], "ctx7sk-abc-aspas-simples",
749            "Must remove single quotes"
750        );
751    }
752
753    #[test]
754    fn test_env_parsing_error_when_no_keys() {
755        let content = "# Apenas comentários\n\
756                        OUTRA_VAR=value\n\
757                        \n";
758        let result = extract_env_keys(content);
759        assert!(
760            result.is_err(),
761            "Must return Err when there are no CONTEXT7_API keys"
762        );
763        let mensagem_erro = result.unwrap_err().to_string();
764        assert!(
765            mensagem_erro.contains("key")
766                || mensagem_erro.contains("CONTEXT7_API")
767                || mensagem_erro.contains("key")
768                || mensagem_erro.contains("API"),
769            "Message de erro deve mencionar CONTEXT7_API, key, key ou API, obteve: {}",
770            mensagem_erro
771        );
772    }
773
774    #[test]
775    fn test_env_parsing_ignores_empty_keys() {
776        let content = "CONTEXT7_API=\n\
777                        CONTEXT7_API=ctx7sk-valida\n";
778        let keys = extract_env_keys(content).expect("Must extract key without error");
779        assert_eq!(
780            keys.len(),
781            1,
782            "Must ignore CONTEXT7_API entries without value"
783        );
784        assert_eq!(keys[0], "ctx7sk-valida");
785    }
786
787    #[test]
788    fn test_env_parsing_ignores_inline_comment() {
789        let content = "CONTEXT7_API=ctx7sk-valida # comentário aqui\n";
790        let keys = extract_env_keys(content).expect("Must extract key without error");
791        assert_eq!(keys.len(), 1);
792        assert_eq!(keys[0], "ctx7sk-valida");
793    }
794
795    // ── B.2: CRLF line endings ────────────────────────────────────────────────
796
797    #[test]
798    fn test_env_parsing_with_crlf_line_endings() {
799        // Arquivo .env gerado no Windows usa \r\n
800        let content = "CONTEXT7_API=ctx7sk-crlf-key-a\r\nCONTEXT7_API=ctx7sk-crlf-key-b\r\n";
801        let keys =
802            extract_env_keys(content).expect("Must extract 2 keys from CRLF content without error");
803        assert_eq!(
804            keys.len(),
805            2,
806            "Must return exactly 2 keys with CRLF"
807        );
808        assert_eq!(
809            keys[0], "ctx7sk-crlf-key-a",
810            "Primeira key não deve conter \\r residual"
811        );
812        assert_eq!(
813            keys[1], "ctx7sk-crlf-key-b",
814            "Segunda key não deve conter \\r residual"
815        );
816    }
817
818    #[test]
819    fn test_env_parsing_with_mixed_line_endings() {
820        // Mix de LF (\n) e CRLF (\r\n) no mesmo file
821        let content = "CONTEXT7_API=ctx7sk-mixed-key-a\nCONTEXT7_API=ctx7sk-mixed-key-b\r\n";
822        let keys = extract_env_keys(content)
823            .expect("Must extract 2 keys from mixed LF/CRLF content without error");
824        assert_eq!(
825            keys.len(),
826            2,
827            "Must return exactly 2 keys with mixed line endings"
828        );
829        assert_eq!(
830            keys[0], "ctx7sk-mixed-key-a",
831            "Chave com LF não deve ter \\r residual"
832        );
833        assert_eq!(
834            keys[1], "ctx7sk-mixed-key-b",
835            "Chave com CRLF não deve ter \\r residual"
836        );
837    }
838
839    // ── mask_key ────────────────────────────────────────────────────────
840
841    #[test]
842    fn test_mask_key_long_value_shows_prefix_and_suffix() {
843        let key = "ctx7sk-abc123-def456-ghi789";
844        assert_eq!(key.len(), 27, "Pré-condição: key deve ter 27 chars");
845        let masked = mask_key(key);
846        assert!(
847            masked.starts_with("ctx7sk-abc12"),
848            "Must start with the first 12 chars, got: {}",
849            masked
850        );
851        assert!(
852            masked.ends_with("i789"),
853            "Must end with the last 4 chars, got: {}",
854            masked
855        );
856        assert!(
857            masked.contains("..."),
858            "Must contain '...' between prefix and suffix, got: {}",
859            masked
860        );
861    }
862
863    #[test]
864    fn test_mask_key_short_returns_asterisks() {
865        let exactly_16_chars_key = "ctx7sk-abcdef012";
866        assert_eq!(
867            exactly_16_chars_key.len(),
868            16,
869            "Pré-condição: key deve ter 16 chars"
870        );
871        let masked = mask_key(exactly_16_chars_key);
872        assert_eq!(
873            masked, "***",
874            "16-char key must return '***', got: {}",
875            masked
876        );
877    }
878
879    #[test]
880    fn test_mask_key_empty_returns_asterisks() {
881        let masked = mask_key("");
882        assert_eq!(
883            masked, "***",
884            "Empty key must return '***', got: {}",
885            masked
886        );
887    }
888
889    #[test]
890    fn test_mask_key_exactly_17_chars_masks_correctly() {
891        let key = "ctx7sk-abcdef0123"; // 17 chars
892        assert_eq!(key.len(), 17, "Pré-condição: key deve ter 17 chars");
893        let masked = mask_key(key);
894        assert!(
895            masked.contains("..."),
896            "Chave de 17 chars deve ser masked, obteve: {}",
897            masked
898        );
899        assert_eq!(
900            &masked[..12],
901            &key[..12],
902            "12-char prefix must be preserved"
903        );
904        assert!(
905            masked.ends_with(&key[key.len() - 4..]),
906            "4-char suffix must be preserved"
907        );
908    }
909
910    // ── read_env_var_key ─────────────────────────────────────────────────────
911
912    #[test]
913    #[serial_test::serial]
914    fn test_read_env_var_key_returns_some_when_set() {
915        // SAFETY: tests serialised via #[serial_test::serial] guarantee absence of
916        // concurrency. Required for compatibility with Rust 2024 edition.
917        unsafe {
918            std::env::set_var("CONTEXT7_API_KEYS", "ctx7sk-key-teste-01");
919        }
920        let result = read_env_var_key();
921        unsafe {
922            std::env::remove_var("CONTEXT7_API_KEYS");
923        }
924
925        let keys = result.expect("Must return Some with valid key");
926        assert_eq!(keys.len(), 1, "Must return exactly 1 key");
927        assert_eq!(keys[0], "ctx7sk-key-teste-01");
928    }
929
930    #[test]
931    #[serial_test::serial]
932    fn test_read_env_var_key_accepts_multiple_comma_separated() {
933        // SAFETY: idem
934        unsafe {
935            std::env::set_var(
936                "CONTEXT7_API_KEYS",
937                "ctx7sk-key-a, ctx7sk-key-b , ctx7sk-key-c",
938            );
939        }
940        let result = read_env_var_key();
941        unsafe {
942            std::env::remove_var("CONTEXT7_API_KEYS");
943        }
944
945        let keys = result.expect("Must return Some with multiple keys");
946        assert_eq!(keys.len(), 3, "Must return 3 keys");
947        assert_eq!(keys[0], "ctx7sk-key-a");
948        assert_eq!(keys[1], "ctx7sk-key-b");
949        assert_eq!(keys[2], "ctx7sk-key-c");
950    }
951
952    #[test]
953    #[serial_test::serial]
954    fn test_read_env_var_key_returns_none_when_empty() {
955        // SAFETY: idem
956        unsafe {
957            std::env::set_var("CONTEXT7_API_KEYS", "");
958        }
959        let result = read_env_var_key();
960        unsafe {
961            std::env::remove_var("CONTEXT7_API_KEYS");
962        }
963
964        assert!(
965            result.is_none(),
966            "Must return None when env var is empty"
967        );
968    }
969
970    #[test]
971    #[serial_test::serial]
972    fn test_read_env_var_key_returns_none_when_only_whitespace() {
973        // SAFETY: idem
974        unsafe {
975            std::env::set_var("CONTEXT7_API_KEYS", "   ,  ,  ");
976        }
977        let result = read_env_var_key();
978        unsafe {
979            std::env::remove_var("CONTEXT7_API_KEYS");
980        }
981
982        assert!(
983            result.is_none(),
984            "Must return None when env var contains only whitespace/commas"
985        );
986    }
987
988    #[test]
989    #[serial_test::serial]
990    fn test_read_env_var_key_returns_none_when_missing() {
991        // SAFETY: idem
992        unsafe {
993            std::env::remove_var("CONTEXT7_API_KEYS");
994        }
995        let result = read_env_var_key();
996
997        assert!(
998            result.is_none(),
999            "Must return None when env var does not exist"
1000        );
1001    }
1002
1003    // ── path traversal via CONTEXT7_HOME ──────────────────────────────────
1004
1005    #[test]
1006    #[serial_test::serial]
1007    fn test_context7_home_rejects_path_traversal() {
1008        let cases = ["../../../etc", "..", "/tmp/../etc"];
1009        for case in &cases {
1010            // SAFETY: env var manipulation in serial test context.
1011            unsafe {
1012                std::env::set_var("CONTEXT7_HOME", case);
1013            }
1014            let result = discover_config_path();
1015            unsafe {
1016                std::env::remove_var("CONTEXT7_HOME");
1017            }
1018
1019            // Must fall back to XDG — the result must NOT contain ".."
1020            if let Some(path) = result {
1021                let s = path.to_string_lossy();
1022                assert!(
1023                    !s.contains(".."),
1024                    "Path traversal '{case}' não deve resultar em path com '..': {s}"
1025                );
1026            }
1027            // None também é aceitável (ProjectDirs ausente no ambiente de CI)
1028        }
1029    }
1030
1031    // ── read_xdg_config via CONTEXT7_HOME ───────────────────────────────────
1032
1033    #[test]
1034    #[serial_test::serial]
1035    fn test_read_xdg_config_returns_none_for_nonexistent_file() {
1036        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
1037        // SAFETY: idem
1038        unsafe {
1039            std::env::set_var("CONTEXT7_HOME", temp_dir.path());
1040        }
1041        let result = read_xdg_config();
1042        unsafe {
1043            std::env::remove_var("CONTEXT7_HOME");
1044        }
1045
1046        let value = result.expect("Must return Ok when file does not exist");
1047        assert!(
1048            value.is_none(),
1049            "Must return None when config.toml does not exist"
1050        );
1051    }
1052
1053    #[test]
1054    #[serial_test::serial]
1055    fn test_read_xdg_config_reads_valid_toml_with_multiple_keys() {
1056        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
1057        let context7_dir = temp_dir.path().join("context7");
1058        std::fs::create_dir_all(&context7_dir).expect("Must create context7 directory");
1059
1060        let toml_content = r#"schema_version = 1
1061
1062[[keys]]
1063value = "ctx7sk-key-xdg-01"
1064added_at = "2026-01-01T00:00:00+00:00"
1065
1066[[keys]]
1067value = "ctx7sk-key-xdg-02"
1068added_at = "2026-01-02T00:00:00+00:00"
1069"#;
1070        std::fs::write(context7_dir.join("config.toml"), toml_content)
1071            .expect("Must write config.toml");
1072
1073        // SAFETY: idem
1074        unsafe {
1075            std::env::set_var("CONTEXT7_HOME", temp_dir.path());
1076        }
1077        let result = read_xdg_config();
1078        unsafe {
1079            std::env::remove_var("CONTEXT7_HOME");
1080        }
1081
1082        let keys = result
1083            .expect("Must return Ok")
1084            .expect("Must return Some with keys");
1085        assert_eq!(keys.len(), 2, "Must return 2 keys");
1086        assert_eq!(keys[0], "ctx7sk-key-xdg-01");
1087        assert_eq!(keys[1], "ctx7sk-key-xdg-02");
1088    }
1089
1090    #[test]
1091    #[serial_test::serial]
1092    fn test_read_xdg_config_returns_err_on_invalid_toml() {
1093        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
1094        let context7_dir = temp_dir.path().join("context7");
1095        std::fs::create_dir_all(&context7_dir).expect("Must create context7 directory");
1096
1097        std::fs::write(
1098            context7_dir.join("config.toml"),
1099            "schema_version = INVALIDO\n[[[malformado",
1100        )
1101        .expect("Must write invalid TOML");
1102
1103        // SAFETY: idem
1104        unsafe {
1105            std::env::set_var("CONTEXT7_HOME", temp_dir.path());
1106        }
1107        let result = read_xdg_config();
1108        unsafe {
1109            std::env::remove_var("CONTEXT7_HOME");
1110        }
1111
1112        assert!(
1113            result.is_err(),
1114            "Must return Err when TOML is malformed"
1115        );
1116    }
1117
1118    #[test]
1119    #[serial_test::serial]
1120    fn test_read_xdg_config_preserves_key_order() {
1121        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
1122        let context7_dir = temp_dir.path().join("context7");
1123        std::fs::create_dir_all(&context7_dir).expect("Must create context7 directory");
1124
1125        let toml_content = r#"schema_version = 1
1126
1127[[keys]]
1128value = "ctx7sk-primeira"
1129added_at = "2026-01-01T00:00:00+00:00"
1130
1131[[keys]]
1132value = "ctx7sk-segunda"
1133added_at = "2026-01-02T00:00:00+00:00"
1134
1135[[keys]]
1136value = "ctx7sk-terceira"
1137added_at = "2026-01-03T00:00:00+00:00"
1138"#;
1139        std::fs::write(context7_dir.join("config.toml"), toml_content)
1140            .expect("Must write config.toml");
1141
1142        // SAFETY: idem
1143        unsafe {
1144            std::env::set_var("CONTEXT7_HOME", temp_dir.path());
1145        }
1146        let result = read_xdg_config();
1147        unsafe {
1148            std::env::remove_var("CONTEXT7_HOME");
1149        }
1150
1151        let keys = result
1152            .expect("Must return Ok")
1153            .expect("Must return Some");
1154        assert_eq!(keys[0], "ctx7sk-primeira");
1155        assert_eq!(keys[1], "ctx7sk-segunda");
1156        assert_eq!(keys[2], "ctx7sk-terceira");
1157    }
1158
1159    #[test]
1160    #[serial_test::serial]
1161    fn test_read_xdg_config_returns_none_for_empty_keys() {
1162        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
1163        let context7_dir = temp_dir.path().join("context7");
1164        std::fs::create_dir_all(&context7_dir).expect("Must create context7 directory");
1165
1166        let toml_without_keys = "schema_version = 1\n";
1167        std::fs::write(context7_dir.join("config.toml"), toml_without_keys)
1168            .expect("Must write config.toml without keys");
1169
1170        // SAFETY: idem
1171        unsafe {
1172            std::env::set_var("CONTEXT7_HOME", temp_dir.path());
1173        }
1174        let result = read_xdg_config();
1175        unsafe {
1176            std::env::remove_var("CONTEXT7_HOME");
1177        }
1178
1179        let value = result.expect("Must return Ok");
1180        assert!(
1181            value.is_none(),
1182            "Must return None when config.toml exists but keys is empty"
1183        );
1184    }
1185
1186    // ── write_xdg_config ───────────────────────────────────────────────────
1187
1188    #[test]
1189    #[serial_test::serial]
1190    fn test_write_xdg_config_roundtrip_serialises_and_deserialises() {
1191        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
1192        // SAFETY: idem
1193        unsafe {
1194            std::env::set_var("CONTEXT7_HOME", temp_dir.path());
1195        }
1196
1197        let path =
1198            write_xdg_config("ctx7sk-roundtrip-01").expect("Must write config without error");
1199
1200        let read_config = read_toml_config_from_path(&path)
1201            .expect("Must read TOML written by write_xdg_config");
1202
1203        unsafe {
1204            std::env::remove_var("CONTEXT7_HOME");
1205        }
1206
1207        assert_eq!(read_config.schema_version, 1, "schema_version must be 1");
1208        assert_eq!(read_config.keys.len(), 1, "Must contain 1 key");
1209        assert_eq!(
1210            read_config.keys[0].value, "ctx7sk-roundtrip-01",
1211            "Valor da key deve ser preservado"
1212        );
1213        assert!(
1214            !read_config.keys[0].added_at.is_empty(),
1215            "added_at não deve ser vazio"
1216        );
1217    }
1218
1219    #[test]
1220    #[serial_test::serial]
1221    fn test_write_xdg_config_creates_parent_dirs_if_missing() {
1222        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
1223        let xdg_novo = temp_dir.path().join("xdg_inexistente");
1224        // SAFETY: idem
1225        unsafe {
1226            std::env::set_var("CONTEXT7_HOME", &xdg_novo);
1227        }
1228
1229        let result = write_xdg_config("ctx7sk-mkdir-teste");
1230        unsafe {
1231            std::env::remove_var("CONTEXT7_HOME");
1232        }
1233
1234        let path = result.expect("Must create parent directory and write config");
1235        assert!(
1236            path.exists(),
1237            "Arquivo de config deve existir após escrita"
1238        );
1239    }
1240
1241    #[test]
1242    #[serial_test::serial]
1243    fn test_write_xdg_config_does_not_duplicate_existing_key() {
1244        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
1245        // SAFETY: idem
1246        unsafe {
1247            std::env::set_var("CONTEXT7_HOME", temp_dir.path());
1248        }
1249
1250        write_xdg_config("ctx7sk-unica").expect("First write must work");
1251        write_xdg_config("ctx7sk-unica").expect("Second write must not fail");
1252
1253        let path = discover_config_path().expect("Must have XDG path");
1254        let config = read_toml_config_from_path(&path).expect("Must read config");
1255
1256        unsafe {
1257            std::env::remove_var("CONTEXT7_HOME");
1258        }
1259
1260        assert_eq!(
1261            config.keys.len(),
1262            1,
1263            "Não deve duplicar key já existente — deve ter apenas 1"
1264        );
1265    }
1266
1267    #[test]
1268    #[serial_test::serial]
1269    fn test_write_xdg_config_accumulates_distinct_keys() {
1270        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
1271        // SAFETY: idem
1272        unsafe {
1273            std::env::set_var("CONTEXT7_HOME", temp_dir.path());
1274        }
1275
1276        write_xdg_config("ctx7sk-key-a").expect("First write must work");
1277        write_xdg_config("ctx7sk-key-b").expect("Second write must work");
1278        write_xdg_config("ctx7sk-key-c").expect("Third write must work");
1279
1280        let path = discover_config_path().expect("Must have XDG path");
1281        let config = read_toml_config_from_path(&path).expect("Must read config");
1282
1283        unsafe {
1284            std::env::remove_var("CONTEXT7_HOME");
1285        }
1286
1287        assert_eq!(config.keys.len(), 3, "Must accumulate 3 distinct keys");
1288        let valores: Vec<&str> = config.keys.iter().map(|c| c.value.as_str()).collect();
1289        assert!(valores.contains(&"ctx7sk-key-a"));
1290        assert!(valores.contains(&"ctx7sk-key-b"));
1291        assert!(valores.contains(&"ctx7sk-key-c"));
1292    }
1293
1294    #[test]
1295    #[cfg(unix)]
1296    #[serial_test::serial]
1297    fn test_write_xdg_config_applies_600_permissions_on_unix() {
1298        use std::os::unix::fs::PermissionsExt;
1299
1300        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
1301        // SAFETY: idem
1302        unsafe {
1303            std::env::set_var("CONTEXT7_HOME", temp_dir.path());
1304        }
1305
1306        let path =
1307            write_xdg_config("ctx7sk-perm-600").expect("Must write config without error");
1308        unsafe {
1309            std::env::remove_var("CONTEXT7_HOME");
1310        }
1311
1312        let metadata = std::fs::metadata(&path).expect("Must obtain file metadata");
1313        let mode = metadata.permissions().mode() & 0o777;
1314
1315        assert_eq!(mode, 0o600, "Permissions must be 600, got: {:o}", mode);
1316    }
1317
1318    // ── Serde TOML roundtrip ──────────────────────────────────────────────────
1319
1320    #[test]
1321    fn test_file_config_serde_roundtrip_preserves_all_fields() {
1322        let original_config = FileConfig {
1323            schema_version: 1,
1324            keys: vec![
1325                StoredKey {
1326                    value: "ctx7sk-serde-01".to_string(),
1327                    added_at: "2026-01-01T12:00:00+00:00".to_string(),
1328                },
1329                StoredKey {
1330                    value: "ctx7sk-serde-02".to_string(),
1331                    added_at: "2026-01-02T12:00:00+00:00".to_string(),
1332                },
1333            ],
1334        };
1335
1336        let toml_str = toml::to_string_pretty(&original_config)
1337            .expect("Must serialise FileConfig to TOML");
1338        let deserialised_config: FileConfig =
1339            toml::from_str(&toml_str).expect("Must deserialise TOML back to FileConfig");
1340
1341        assert_eq!(
1342            deserialised_config.schema_version, original_config.schema_version,
1343            "schema_version must be preserved in roundtrip"
1344        );
1345        assert_eq!(
1346            deserialised_config.keys.len(),
1347            original_config.keys.len(),
1348            "Número de keys deve ser preservado"
1349        );
1350        assert_eq!(
1351            deserialised_config.keys[0].value, original_config.keys[0].value,
1352            "Valor da primeira key deve ser preservado"
1353        );
1354        assert_eq!(
1355            deserialised_config.keys[0].added_at, original_config.keys[0].added_at,
1356            "added_at da primeira key deve ser preservado"
1357        );
1358    }
1359
1360    #[test]
1361    fn test_file_config_schema_version_always_present_in_serialisation() {
1362        let config = FileConfig {
1363            schema_version: 1,
1364            keys: Vec::new(),
1365        };
1366
1367        let toml_str = toml::to_string_pretty(&config).expect("Must serialise to TOML");
1368
1369        assert!(
1370            toml_str.contains("schema_version"),
1371            "schema_version must be present in TOML serialisation"
1372        );
1373        assert!(toml_str.contains('1'), "Value 1 must be present");
1374    }
1375
1376    #[test]
1377    fn test_file_config_empty_keys_accepted_in_deserialisation() {
1378        let toml_str = "schema_version = 1\n";
1379        let config: FileConfig =
1380            toml::from_str(toml_str).expect("Must deserialise with keys absent (empty default)");
1381
1382        assert_eq!(config.schema_version, 1);
1383        assert!(
1384            config.keys.is_empty(),
1385            "keys must be empty when not present in TOML"
1386        );
1387    }
1388
1389    #[test]
1390    fn test_stored_key_preserves_added_at_as_utc_string() {
1391        let timestamp = "2026-04-08T20:00:00+00:00";
1392        let key = StoredKey {
1393            value: "ctx7sk-timestamp".to_string(),
1394            added_at: timestamp.to_string(),
1395        };
1396
1397        let toml_str = toml::to_string_pretty(&key).expect("Must serialise StoredKey");
1398        let chave_de_volta: StoredKey =
1399            toml::from_str(&toml_str).expect("Must deserialise StoredKey");
1400
1401        assert_eq!(
1402            chave_de_volta.added_at, timestamp,
1403            "Timestamp added_at deve ser preservado exactly"
1404        );
1405    }
1406
1407    // ── load_api_keys (precedence) ─────────────────────────────────────
1408
1409    #[test]
1410    #[serial_test::serial]
1411    fn test_load_api_keys_env_var_takes_priority_over_xdg() {
1412        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
1413        let context7_dir = temp_dir.path().join("context7");
1414        std::fs::create_dir_all(&context7_dir).expect("Must create context7 directory");
1415
1416        let toml_xdg = r#"schema_version = 1
1417[[keys]]
1418value = "ctx7sk-xdg-deve-ser-ignorada"
1419added_at = "2026-01-01T00:00:00+00:00"
1420"#;
1421        std::fs::write(context7_dir.join("config.toml"), toml_xdg)
1422            .expect("Must write XDG config");
1423
1424        // SAFETY: idem
1425        unsafe {
1426            std::env::set_var("CONTEXT7_API_KEYS", "ctx7sk-env-var-prioritaria");
1427            std::env::set_var("CONTEXT7_HOME", temp_dir.path());
1428        }
1429
1430        let result = load_api_keys();
1431
1432        unsafe {
1433            std::env::remove_var("CONTEXT7_API_KEYS");
1434            std::env::remove_var("CONTEXT7_HOME");
1435        }
1436
1437        let keys = result.expect("Must load keys via env var");
1438        assert_eq!(keys.len(), 1);
1439        assert_eq!(
1440            keys[0], "ctx7sk-env-var-prioritaria",
1441            "Env var deve ter prioridade sobre XDG"
1442        );
1443    }
1444
1445    #[test]
1446    #[serial_test::serial]
1447    fn test_load_api_keys_xdg_used_when_env_var_missing() {
1448        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
1449        let context7_dir = temp_dir.path().join("context7");
1450        std::fs::create_dir_all(&context7_dir).expect("Must create context7 directory");
1451
1452        let toml_xdg = r#"schema_version = 1
1453[[keys]]
1454value = "ctx7sk-via-xdg"
1455added_at = "2026-01-01T00:00:00+00:00"
1456"#;
1457        std::fs::write(context7_dir.join("config.toml"), toml_xdg)
1458            .expect("Must write XDG config");
1459
1460        // SAFETY: idem
1461        unsafe {
1462            std::env::remove_var("CONTEXT7_API_KEYS");
1463            std::env::set_var("CONTEXT7_HOME", temp_dir.path());
1464        }
1465
1466        let result = load_api_keys();
1467
1468        unsafe {
1469            std::env::remove_var("CONTEXT7_HOME");
1470        }
1471
1472        let keys = result.expect("Must load keys via XDG");
1473        assert_eq!(keys.len(), 1);
1474        assert_eq!(keys[0], "ctx7sk-via-xdg");
1475    }
1476
1477    #[test]
1478    #[serial_test::serial]
1479    fn test_load_api_keys_returns_err_when_nothing_available() {
1480        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
1481        let empty_xdg_dir = temp_dir.path().join("xdg_vazio");
1482        std::fs::create_dir_all(&empty_xdg_dir).expect("Must create empty XDG directory");
1483
1484        let no_env_dir = temp_dir.path().join("sem_env");
1485        std::fs::create_dir_all(&no_env_dir).expect("Must create directory without .env");
1486
1487        // SAFETY: idem
1488        unsafe {
1489            std::env::remove_var("CONTEXT7_API_KEYS");
1490            std::env::set_var("CONTEXT7_HOME", &empty_xdg_dir);
1491        }
1492        let original_cwd = std::env::current_dir().expect("Must obtain current CWD");
1493        std::env::set_current_dir(&no_env_dir).expect("Must change CWD");
1494
1495        let result = load_api_keys();
1496
1497        std::env::set_current_dir(&original_cwd).expect("Must restore CWD");
1498        unsafe {
1499            std::env::remove_var("CONTEXT7_HOME");
1500        }
1501
1502        assert!(
1503            result.is_err(),
1504            "Must return Err when no layer provides keys"
1505        );
1506    }
1507
1508    #[test]
1509    #[serial_test::serial]
1510    fn test_read_env_cwd_reads_env_with_multiple_context7_api_keys() {
1511        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
1512        let conteudo_env = "CONTEXT7_API=ctx7sk-cwd-01\nCONTEXT7_API=ctx7sk-cwd-02\n";
1513        std::fs::write(temp_dir.path().join(".env"), conteudo_env)
1514            .expect("Must write temporary .env");
1515
1516        let original_cwd = std::env::current_dir().expect("Must obtain CWD");
1517        std::env::set_current_dir(temp_dir.path()).expect("Must change CWD to temp");
1518
1519        let result = read_env_cwd();
1520
1521        std::env::set_current_dir(&original_cwd).expect("Must restore CWD");
1522
1523        let keys = result.expect("Must return Some with keys from CWD .env");
1524        assert_eq!(keys.len(), 2, "Must read 2 keys from .env");
1525        assert_eq!(keys[0], "ctx7sk-cwd-01");
1526        assert_eq!(keys[1], "ctx7sk-cwd-02");
1527    }
1528
1529    #[test]
1530    #[serial_test::serial]
1531    fn test_read_env_cwd_returns_none_when_env_missing() {
1532        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
1533
1534        let original_cwd = std::env::current_dir().expect("Must obtain CWD");
1535        std::env::set_current_dir(temp_dir.path()).expect("Must change CWD to temp without .env");
1536
1537        let result = read_env_cwd();
1538
1539        std::env::set_current_dir(&original_cwd).expect("Must restore CWD");
1540
1541        assert!(
1542            result.is_none(),
1543            "Must return None when there is no .env in CWD"
1544        );
1545    }
1546
1547    #[test]
1548    fn test_discover_xdg_log_paths_returns_some_valid_path() {
1549        let result = discover_xdg_log_paths();
1550
1551        if let Some(path) = result {
1552            let path_str = path.to_string_lossy();
1553            assert!(
1554                path_str.contains("context7"),
1555                "Caminho de logs XDG deve conter 'context7', obteve: {}",
1556                path_str
1557            );
1558        }
1559    }
1560
1561    #[test]
1562    #[serial_test::serial]
1563    fn test_load_api_keys_env_cwd_used_when_env_var_and_xdg_missing() {
1564        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
1565        let xdg_dir_no_config = temp_dir.path().join("xdg_sem_config");
1566        std::fs::create_dir_all(&xdg_dir_no_config).expect("Must create empty XDG directory");
1567
1568        let cwd_dir = temp_dir.path().join("cwd_com_env");
1569        std::fs::create_dir_all(&cwd_dir).expect("Must create temporary CWD");
1570        std::fs::write(cwd_dir.join(".env"), "CONTEXT7_API=ctx7sk-cwd-camada-3\n")
1571            .expect("Must write .env in CWD");
1572
1573        // SAFETY: idem
1574        unsafe {
1575            std::env::remove_var("CONTEXT7_API_KEYS");
1576            std::env::set_var("CONTEXT7_HOME", &xdg_dir_no_config);
1577        }
1578        let original_cwd = std::env::current_dir().expect("Must obtain CWD");
1579        std::env::set_current_dir(&cwd_dir).expect("Must change CWD");
1580
1581        let result = load_api_keys();
1582
1583        std::env::set_current_dir(&original_cwd).expect("Must restore CWD");
1584        unsafe {
1585            std::env::remove_var("CONTEXT7_HOME");
1586        }
1587
1588        let keys = result.expect("Must load keys via CWD .env");
1589        assert_eq!(keys.len(), 1);
1590        assert_eq!(keys[0], "ctx7sk-cwd-camada-3");
1591    }
1592
1593    #[test]
1594    #[serial_test::serial]
1595    fn test_load_api_keys_falls_back_when_xdg_invalid() {
1596        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
1597        let context7_dir = temp_dir.path().join("context7");
1598        std::fs::create_dir_all(&context7_dir).expect("Must create context7 directory");
1599
1600        std::fs::write(context7_dir.join("config.toml"), "[[[invalido")
1601            .expect("Must write invalid TOML");
1602
1603        let cwd_dir = temp_dir.path().join("cwd_fallback");
1604        std::fs::create_dir_all(&cwd_dir).expect("Must create CWD with .env");
1605        std::fs::write(cwd_dir.join(".env"), "CONTEXT7_API=ctx7sk-fallback-cwd\n")
1606            .expect("Must write .env in CWD");
1607
1608        // SAFETY: idem
1609        unsafe {
1610            std::env::remove_var("CONTEXT7_API_KEYS");
1611            std::env::set_var("CONTEXT7_HOME", temp_dir.path());
1612        }
1613        let original_cwd = std::env::current_dir().expect("Must obtain CWD");
1614        std::env::set_current_dir(&cwd_dir).expect("Must change CWD");
1615
1616        let result = load_api_keys();
1617
1618        std::env::set_current_dir(&original_cwd).expect("Must restore CWD");
1619        unsafe {
1620            std::env::remove_var("CONTEXT7_HOME");
1621        }
1622
1623        let keys = result.expect("Must load keys via fallback CWD .env");
1624        assert_eq!(keys.len(), 1);
1625        assert_eq!(keys[0], "ctx7sk-fallback-cwd");
1626    }
1627
1628    // ── cmd_keys_add ─────────────────────────────────────────────────────────
1629
1630    #[test]
1631    #[serial_test::serial]
1632    fn test_cmd_keys_add_creates_config_when_missing() {
1633        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
1634        // SAFETY: idem
1635        unsafe {
1636            std::env::set_var("CONTEXT7_HOME", temp_dir.path());
1637        }
1638
1639        let result = cmd_keys_add("ctx7sk-nova-key-add-test");
1640
1641        unsafe {
1642            std::env::remove_var("CONTEXT7_HOME");
1643        }
1644
1645        result.expect("cmd_keys_add deve funcionar em config vazio");
1646
1647        let path = temp_dir.path().join("context7").join("config.toml");
1648        assert!(
1649            path.exists(),
1650            "config.toml deve existir após cmd_keys_add"
1651        );
1652
1653        let config = read_toml_config_from_path(&path).expect("Must read created config");
1654        assert_eq!(config.keys.len(), 1, "Config deve ter 1 key");
1655        assert_eq!(config.keys[0].value, "ctx7sk-nova-key-add-test");
1656    }
1657
1658    #[test]
1659    #[serial_test::serial]
1660    fn test_cmd_keys_add_accumulates_in_existing_config() {
1661        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
1662        // SAFETY: idem
1663        unsafe {
1664            std::env::set_var("CONTEXT7_HOME", temp_dir.path());
1665        }
1666
1667        cmd_keys_add("ctx7sk-key-um").expect("Primeira adição deve funcionar");
1668        cmd_keys_add("ctx7sk-key-dois").expect("Segunda adição deve funcionar");
1669
1670        let path = discover_config_path().expect("Must have XDG path");
1671        let config = read_toml_config_from_path(&path).expect("Must read config");
1672
1673        unsafe {
1674            std::env::remove_var("CONTEXT7_HOME");
1675        }
1676
1677        assert_eq!(config.keys.len(), 2, "Must accumulate 2 keys");
1678        assert_eq!(config.keys[0].value, "ctx7sk-key-um");
1679        assert_eq!(config.keys[1].value, "ctx7sk-key-dois");
1680    }
1681
1682    #[test]
1683    #[serial_test::serial]
1684    fn test_cmd_keys_add_does_not_duplicate_existing_key() {
1685        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
1686        // SAFETY: idem
1687        unsafe {
1688            std::env::set_var("CONTEXT7_HOME", temp_dir.path());
1689        }
1690
1691        cmd_keys_add("ctx7sk-unica-dedup").expect("Primeira adição deve funcionar");
1692        cmd_keys_add("ctx7sk-unica-dedup").expect("Segunda adição da mesma key não deve falhar");
1693
1694        let path = discover_config_path().expect("Must have XDG path");
1695        let config = read_toml_config_from_path(&path).expect("Must read config");
1696
1697        unsafe {
1698            std::env::remove_var("CONTEXT7_HOME");
1699        }
1700
1701        assert_eq!(config.keys.len(), 1, "Não deve duplicar key já existente");
1702    }
1703
1704    #[test]
1705    #[cfg(unix)]
1706    #[serial_test::serial]
1707    fn test_cmd_keys_add_applies_600_permissions_on_unix() {
1708        use std::os::unix::fs::PermissionsExt;
1709
1710        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
1711        // SAFETY: idem
1712        unsafe {
1713            std::env::set_var("CONTEXT7_HOME", temp_dir.path());
1714        }
1715
1716        cmd_keys_add("ctx7sk-perm-600-keys-add").expect("Must add key without error");
1717
1718        let path = discover_config_path().expect("Must have XDG path");
1719        unsafe {
1720            std::env::remove_var("CONTEXT7_HOME");
1721        }
1722
1723        let metadata = std::fs::metadata(&path).expect("Must obtain metadata");
1724        let mode = metadata.permissions().mode() & 0o777;
1725        assert_eq!(
1726            mode, 0o600,
1727            "Permissões devem ser 600 após cmd_keys_add, obteve: {:o}",
1728            mode
1729        );
1730    }
1731
1732    // ── cmd_keys_remove ───────────────────────────────────────────────────────
1733
1734    #[test]
1735    #[serial_test::serial]
1736    fn test_cmd_keys_remove_index_1_from_config_with_3_keys() {
1737        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
1738        // SAFETY: idem
1739        unsafe {
1740            std::env::set_var("CONTEXT7_HOME", temp_dir.path());
1741        }
1742
1743        write_xdg_config("ctx7sk-rem-alpha").expect("Must write key 1");
1744        write_xdg_config("ctx7sk-rem-beta").expect("Must write key 2");
1745        write_xdg_config("ctx7sk-rem-gamma").expect("Must write key 3");
1746
1747        cmd_keys_remove(1).expect("Remove index 1 must work");
1748
1749        let path = discover_config_path().expect("Must have XDG path");
1750        let config = read_toml_config_from_path(&path).expect("Must read config");
1751
1752        unsafe {
1753            std::env::remove_var("CONTEXT7_HOME");
1754        }
1755
1756        assert_eq!(config.keys.len(), 2, "Must remain 2 keys after removal");
1757        assert_eq!(config.keys[0].value, "ctx7sk-rem-beta");
1758        assert_eq!(config.keys[1].value, "ctx7sk-rem-gamma");
1759    }
1760
1761    #[test]
1762    #[serial_test::serial]
1763    fn test_cmd_keys_remove_index_2_from_config_with_3_keys() {
1764        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
1765        // SAFETY: idem
1766        unsafe {
1767            std::env::set_var("CONTEXT7_HOME", temp_dir.path());
1768        }
1769
1770        write_xdg_config("ctx7sk-mid-alpha").expect("Must write key 1");
1771        write_xdg_config("ctx7sk-mid-beta").expect("Must write key 2");
1772        write_xdg_config("ctx7sk-mid-gamma").expect("Must write key 3");
1773
1774        cmd_keys_remove(2).expect("Remove index 2 must work");
1775
1776        let path = discover_config_path().expect("Must have XDG path");
1777        let config = read_toml_config_from_path(&path).expect("Must read config");
1778
1779        unsafe {
1780            std::env::remove_var("CONTEXT7_HOME");
1781        }
1782
1783        assert_eq!(
1784            config.keys.len(),
1785            2,
1786            "Must remain 2 keys after removing the middle one"
1787        );
1788        assert_eq!(config.keys[0].value, "ctx7sk-mid-alpha");
1789        assert_eq!(config.keys[1].value, "ctx7sk-mid-gamma");
1790    }
1791
1792    #[test]
1793    #[serial_test::serial]
1794    fn test_cmd_keys_remove_index_zero_returns_err_with_message() {
1795        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
1796        // SAFETY: idem
1797        unsafe {
1798            std::env::set_var("CONTEXT7_HOME", temp_dir.path());
1799        }
1800
1801        write_xdg_config("ctx7sk-idx-zero-test").expect("Must write key");
1802
1803        let result = cmd_keys_remove(0);
1804
1805        unsafe {
1806            std::env::remove_var("CONTEXT7_HOME");
1807        }
1808
1809        assert!(
1810            result.is_err(),
1811            "Invalid index 0 must return Err (exit code 1), got: {:?}",
1812            result
1813        );
1814    }
1815
1816    #[test]
1817    #[serial_test::serial]
1818    fn test_cmd_keys_remove_index_greater_than_len_returns_err_with_message() {
1819        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
1820        // SAFETY: idem
1821        unsafe {
1822            std::env::set_var("CONTEXT7_HOME", temp_dir.path());
1823        }
1824
1825        write_xdg_config("ctx7sk-overflow-test").expect("Must write key");
1826
1827        let result = cmd_keys_remove(99);
1828
1829        unsafe {
1830            std::env::remove_var("CONTEXT7_HOME");
1831        }
1832
1833        assert!(
1834            result.is_err(),
1835            "Index out of range must return Err (exit code 1), got: {:?}",
1836            result
1837        );
1838    }
1839
1840    #[test]
1841    #[serial_test::serial]
1842    fn test_cmd_keys_remove_from_empty_config_returns_err() {
1843        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
1844        // SAFETY: idem
1845        unsafe {
1846            std::env::set_var("CONTEXT7_HOME", temp_dir.path());
1847        }
1848
1849        let result = cmd_keys_remove(1);
1850
1851        unsafe {
1852            std::env::remove_var("CONTEXT7_HOME");
1853        }
1854
1855        assert!(
1856            result.is_err(),
1857            "Remove from empty config must return Err (exit code 1), got: {:?}",
1858            result
1859        );
1860    }
1861
1862    // ── cmd_keys_clear ────────────────────────────────────────────────────────
1863
1864    #[test]
1865    #[serial_test::serial]
1866    fn test_cmd_keys_clear_with_yes_true_clears_all_keys() {
1867        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
1868        // SAFETY: idem
1869        unsafe {
1870            std::env::set_var("CONTEXT7_HOME", temp_dir.path());
1871        }
1872
1873        write_xdg_config("ctx7sk-clear-alpha").expect("Must write key 1");
1874        write_xdg_config("ctx7sk-clear-beta").expect("Must write key 2");
1875
1876        let path = discover_config_path().expect("Must have XDG path");
1877        let before = read_toml_config_from_path(&path).expect("Must read before config");
1878        assert_eq!(before.keys.len(), 2, "Pré-condição: 2 keys before do clear");
1879
1880        cmd_keys_clear(true).expect("clear with yes=true must work");
1881
1882        let after = read_toml_config_from_path(&path).expect("Must read after config");
1883
1884        unsafe {
1885            std::env::remove_var("CONTEXT7_HOME");
1886        }
1887
1888        assert!(
1889            after.keys.is_empty(),
1890            "Após clear com yes=true, keys devem estar vazias"
1891        );
1892    }
1893
1894    #[test]
1895    #[serial_test::serial]
1896    fn test_cmd_keys_clear_with_yes_true_works_on_nonexistent_config() {
1897        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
1898        // SAFETY: idem
1899        unsafe {
1900            std::env::set_var("CONTEXT7_HOME", temp_dir.path());
1901        }
1902
1903        let result = cmd_keys_clear(true);
1904
1905        unsafe {
1906            std::env::remove_var("CONTEXT7_HOME");
1907        }
1908
1909        assert!(
1910            result.is_ok(),
1911            "clear on nonexistent config must return Ok (idempotent), got: {:?}",
1912            result
1913        );
1914    }
1915
1916    // ── cmd_keys_import ───────────────────────────────────────────────────────
1917
1918    #[test]
1919    #[serial_test::serial]
1920    fn test_cmd_keys_import_valid_env_with_multiple_keys() {
1921        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
1922        let arquivo_env = temp_dir.path().join("keys.env");
1923        std::fs::write(
1924            &arquivo_env,
1925            "CONTEXT7_API=ctx7sk-import-alpha\nCONTEXT7_API=ctx7sk-import-beta\n",
1926        )
1927        .expect("Must write test .env");
1928
1929        // SAFETY: idem
1930        unsafe {
1931            std::env::set_var("CONTEXT7_HOME", temp_dir.path());
1932        }
1933
1934        let result = cmd_keys_import(&arquivo_env);
1935
1936        let path = discover_config_path().expect("Must have XDG path");
1937        let config = read_toml_config_from_path(&path).expect("Must read config after import");
1938
1939        unsafe {
1940            std::env::remove_var("CONTEXT7_HOME");
1941        }
1942
1943        result.expect("valid .env import must work");
1944        assert_eq!(config.keys.len(), 2, "Must have imported 2 keys");
1945
1946        let valores: Vec<&str> = config.keys.iter().map(|c| c.value.as_str()).collect();
1947        assert!(valores.contains(&"ctx7sk-import-alpha"));
1948        assert!(valores.contains(&"ctx7sk-import-beta"));
1949    }
1950
1951    #[test]
1952    #[serial_test::serial]
1953    fn test_cmd_keys_import_env_without_keys_returns_err() {
1954        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
1955        let arquivo_env = temp_dir.path().join("vazio.env");
1956        std::fs::write(&arquivo_env, "# apenas comentario\nOUTRA_VAR=value\n")
1957            .expect("Must write .env without keys");
1958
1959        // SAFETY: idem
1960        unsafe {
1961            std::env::set_var("CONTEXT7_HOME", temp_dir.path());
1962        }
1963
1964        let result = cmd_keys_import(&arquivo_env);
1965
1966        unsafe {
1967            std::env::remove_var("CONTEXT7_HOME");
1968        }
1969
1970        assert!(
1971            result.is_err(),
1972            "Import de .env sem keys CONTEXT7_API deve retornar Err"
1973        );
1974    }
1975
1976    #[test]
1977    #[serial_test::serial]
1978    fn test_cmd_keys_import_nonexistent_file_returns_err() {
1979        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
1980        let arquivo_inexistente = temp_dir.path().join("nao_existe.env");
1981
1982        // SAFETY: idem
1983        unsafe {
1984            std::env::set_var("CONTEXT7_HOME", temp_dir.path());
1985        }
1986
1987        let result = cmd_keys_import(&arquivo_inexistente);
1988
1989        unsafe {
1990            std::env::remove_var("CONTEXT7_HOME");
1991        }
1992
1993        assert!(
1994            result.is_err(),
1995            "Import de file inexistente deve retornar Err"
1996        );
1997    }
1998
1999    #[test]
2000    #[serial_test::serial]
2001    fn test_cmd_keys_import_roundtrip_add_then_list() {
2002        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
2003        let arquivo_env = temp_dir.path().join("roundtrip.env");
2004        std::fs::write(
2005            &arquivo_env,
2006            "CONTEXT7_API=ctx7sk-rtrip-01\nCONTEXT7_API=ctx7sk-rtrip-02\n",
2007        )
2008        .expect("Must write roundtrip .env");
2009
2010        // SAFETY: idem
2011        unsafe {
2012            std::env::set_var("CONTEXT7_HOME", temp_dir.path());
2013        }
2014
2015        cmd_keys_import(&arquivo_env).expect("Import must work");
2016
2017        let config = read_xdg_config_raw()
2018            .expect("Must return Ok")
2019            .expect("Must return Some after import");
2020
2021        unsafe {
2022            std::env::remove_var("CONTEXT7_HOME");
2023        }
2024
2025        assert_eq!(
2026            config.keys.len(),
2027            2,
2028            "Roundtrip: deve ter 2 keys após import"
2029        );
2030        assert_eq!(config.keys[0].value, "ctx7sk-rtrip-01");
2031        assert_eq!(config.keys[1].value, "ctx7sk-rtrip-02");
2032    }
2033
2034    // ── cmd_keys_export ───────────────────────────────────────────────────────
2035
2036    #[test]
2037    #[serial_test::serial]
2038    fn test_cmd_keys_export_empty_config_returns_ok() {
2039        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
2040        // SAFETY: idem
2041        unsafe {
2042            std::env::set_var("CONTEXT7_HOME", temp_dir.path());
2043        }
2044
2045        let result = cmd_keys_export();
2046
2047        unsafe {
2048            std::env::remove_var("CONTEXT7_HOME");
2049        }
2050
2051        assert!(
2052            result.is_ok(),
2053            "Export of empty config must return Ok, got: {:?}",
2054            result
2055        );
2056    }
2057
2058    #[test]
2059    #[serial_test::serial]
2060    fn test_cmd_keys_export_returns_ok_with_existing_keys() {
2061        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
2062        // SAFETY: idem
2063        unsafe {
2064            std::env::set_var("CONTEXT7_HOME", temp_dir.path());
2065        }
2066
2067        write_xdg_config("ctx7sk-export-um").expect("Must write key 1");
2068        write_xdg_config("ctx7sk-export-dois").expect("Must write key 2");
2069
2070        let result = cmd_keys_export();
2071
2072        unsafe {
2073            std::env::remove_var("CONTEXT7_HOME");
2074        }
2075
2076        assert!(
2077            result.is_ok(),
2078            "Export com keys existentes deve retornar Ok, obteve: {:?}",
2079            result
2080        );
2081    }
2082
2083    // ── read_xdg_config_raw ────────────────────────────────────────────────────
2084
2085    #[test]
2086    #[serial_test::serial]
2087    fn test_read_xdg_config_raw_returns_none_without_file() {
2088        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
2089        // SAFETY: idem
2090        unsafe {
2091            std::env::set_var("CONTEXT7_HOME", temp_dir.path());
2092        }
2093
2094        let result = read_xdg_config_raw();
2095
2096        unsafe {
2097            std::env::remove_var("CONTEXT7_HOME");
2098        }
2099
2100        let value = result.expect("Must return Ok");
2101        assert!(
2102            value.is_none(),
2103            "Must return None when config.toml does not exist"
2104        );
2105    }
2106
2107    #[test]
2108    #[serial_test::serial]
2109    fn test_read_xdg_config_raw_returns_config_with_keys() {
2110        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
2111        let context7_dir = temp_dir.path().join("context7");
2112        std::fs::create_dir_all(&context7_dir).expect("Must create context7 directory");
2113
2114        let toml = r#"schema_version = 1
2115
2116[[keys]]
2117value = "ctx7sk-raw-01"
2118added_at = "2026-04-08T00:00:00+00:00"
2119
2120[[keys]]
2121value = "ctx7sk-raw-02"
2122added_at = "2026-04-08T00:01:00+00:00"
2123"#;
2124        std::fs::write(context7_dir.join("config.toml"), toml).expect("Must write config.toml");
2125
2126        // SAFETY: idem
2127        unsafe {
2128            std::env::set_var("CONTEXT7_HOME", temp_dir.path());
2129        }
2130
2131        let result = read_xdg_config_raw();
2132
2133        unsafe {
2134            std::env::remove_var("CONTEXT7_HOME");
2135        }
2136
2137        let config = result
2138            .expect("Must return Ok")
2139            .expect("Must return Some with config");
2140        assert_eq!(config.keys.len(), 2);
2141        assert_eq!(config.keys[0].value, "ctx7sk-raw-01");
2142        assert_eq!(config.keys[1].value, "ctx7sk-raw-02");
2143    }
2144
2145    #[test]
2146    #[serial_test::serial]
2147    fn test_cmd_keys_path_returns_ok() {
2148        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
2149        // SAFETY: idem
2150        unsafe {
2151            std::env::set_var("CONTEXT7_HOME", temp_dir.path());
2152        }
2153
2154        let result = cmd_keys_path();
2155
2156        unsafe {
2157            std::env::remove_var("CONTEXT7_HOME");
2158        }
2159
2160        result.expect("cmd_keys_path must return Ok");
2161    }
2162
2163    #[test]
2164    #[serial_test::serial]
2165    fn test_discover_config_path_ends_with_config_toml() {
2166        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
2167        // SAFETY: idem
2168        unsafe {
2169            std::env::set_var("CONTEXT7_HOME", temp_dir.path());
2170        }
2171
2172        let path = discover_config_path();
2173
2174        unsafe {
2175            std::env::remove_var("CONTEXT7_HOME");
2176        }
2177
2178        let path = path.expect("Must return valid XDG path");
2179        assert!(
2180            path.to_string_lossy().ends_with("config.toml"),
2181            "Path must end with config.toml, got: {}",
2182            path.display()
2183        );
2184        assert!(
2185            path.to_string_lossy().contains("context7"),
2186            "Path must contain 'context7', got: {}",
2187            path.display()
2188        );
2189    }
2190
2191    // ── fluxo completo ────────────────────────────────────────────────────────
2192
2193    #[test]
2194    #[serial_test::serial]
2195    fn test_complete_flow_add_list_remove_clear() {
2196        let temp_dir = tempfile::TempDir::new().expect("Must create temporary directory");
2197        // SAFETY: idem
2198        unsafe {
2199            std::env::set_var("CONTEXT7_HOME", temp_dir.path());
2200        }
2201
2202        cmd_keys_add("ctx7sk-fluxo-01").expect("Add 1 must work");
2203        cmd_keys_add("ctx7sk-fluxo-02").expect("Add 2 must work");
2204        cmd_keys_add("ctx7sk-fluxo-03").expect("Add 3 must work");
2205
2206        let config_before = read_xdg_config_raw()
2207            .expect("Ok")
2208            .expect("Some com 3 keys");
2209        assert_eq!(config_before.keys.len(), 3, "Must have 3 keys after 3 adds");
2210
2211        cmd_keys_remove(2).expect("Remove index 2 must work");
2212
2213        let config_after_remove = read_xdg_config_raw()
2214            .expect("Ok")
2215            .expect("Some com 2 keys");
2216        assert_eq!(
2217            config_after_remove.keys.len(),
2218            2,
2219            "Must have 2 keys after remove"
2220        );
2221        assert_eq!(config_after_remove.keys[0].value, "ctx7sk-fluxo-01");
2222        assert_eq!(config_after_remove.keys[1].value, "ctx7sk-fluxo-03");
2223
2224        cmd_keys_clear(true).expect("Clear com yes=true deve funcionar");
2225
2226        let path = discover_config_path().expect("Must have path");
2227        let final_config = read_toml_config_from_path(&path).expect("Must read final config");
2228
2229        unsafe {
2230            std::env::remove_var("CONTEXT7_HOME");
2231        }
2232
2233        assert!(
2234            final_config.keys.is_empty(),
2235            "Após clear, keys devem estar vazias"
2236        );
2237    }
2238
2239    // ── CONTEXT7_HOME override direto ─────────────────────────────────────────
2240
2241    #[test]
2242    #[serial_test::serial]
2243    fn test_context7_home_override_config_path() {
2244        let tmp = tempfile::TempDir::new().expect("Must create tempdir");
2245        // SAFETY: tests serialised via #[serial_test::serial] guarantee absence of
2246        // concurrency. Required for compatibility with Rust 2024 edition.
2247        unsafe {
2248            std::env::set_var("CONTEXT7_HOME", tmp.path());
2249        }
2250
2251        let path = discover_config_path();
2252
2253        unsafe {
2254            std::env::remove_var("CONTEXT7_HOME");
2255        }
2256
2257        let path = path.expect("Must return Some when CONTEXT7_HOME is defined");
2258        let expected = tmp.path().join("context7").join("config.toml");
2259        assert_eq!(
2260            path, expected,
2261            "CONTEXT7_HOME must define path as {{CONTEXT7_HOME}}/context7/config.toml"
2262        );
2263    }
2264
2265    #[test]
2266    #[serial_test::serial]
2267    fn test_context7_home_override_logs_path() {
2268        let tmp = tempfile::TempDir::new().expect("Must create tempdir");
2269        // SAFETY: idem
2270        unsafe {
2271            std::env::set_var("CONTEXT7_HOME", tmp.path());
2272        }
2273
2274        let path = discover_xdg_log_paths();
2275
2276        unsafe {
2277            std::env::remove_var("CONTEXT7_HOME");
2278        }
2279
2280        let path = path.expect("Must return Some when CONTEXT7_HOME is defined");
2281        let expected = tmp.path().join("context7").join("logs");
2282        assert_eq!(
2283            path, expected,
2284            "CONTEXT7_HOME must define logs as {{CONTEXT7_HOME}}/context7/logs"
2285        );
2286    }
2287
2288    #[test]
2289    #[serial_test::serial]
2290    fn test_context7_home_empty_falls_back_to_projectdirs() {
2291        let tmp = tempfile::TempDir::new().expect("Must create tempdir");
2292        // SAFETY: idem
2293        unsafe {
2294            std::env::set_var("CONTEXT7_HOME", "");
2295        }
2296
2297        let path = discover_config_path();
2298
2299        unsafe {
2300            std::env::remove_var("CONTEXT7_HOME");
2301        }
2302
2303        // When CONTEXT7_HOME is empty, falls back to ProjectDirs — path must NOT be inside the tempdir
2304        if let Some(c) = path {
2305            let tmp_str = tmp.path().to_string_lossy();
2306            assert!(
2307                !c.to_string_lossy().starts_with(tmp_str.as_ref()),
2308                "CONTEXT7_HOME empty must not use the tempdir: {}",
2309                c.display()
2310            );
2311        }
2312        // If ProjectDirs returns None (CI without home), that is also acceptable
2313    }
2314}