Skip to main content

lineark_sdk/
auth.rs

1//! API token resolution.
2//!
3//! Supports three sources (in precedence order): explicit token, the
4//! `LINEAR_API_TOKEN` environment variable, and `~/.linear_api_token` file.
5
6use crate::error::LinearError;
7use std::path::PathBuf;
8
9/// Resolve a Linear API token from the filesystem.
10/// Reads `~/.linear_api_token` (linearis-compatible).
11pub fn token_from_file() -> Result<String, LinearError> {
12    let path = token_file_path()?;
13    std::fs::read_to_string(&path)
14        .map(|s| s.trim().to_string())
15        .map_err(|e| {
16            LinearError::AuthConfig(format!(
17                "Could not read token file {}: {}",
18                path.display(),
19                e
20            ))
21        })
22}
23
24/// Resolve a Linear API token from the environment variable `LINEAR_API_TOKEN`.
25pub fn token_from_env() -> Result<String, LinearError> {
26    match std::env::var("LINEAR_API_TOKEN") {
27        Ok(val) if !val.trim().is_empty() => Ok(val.trim().to_string()),
28        _ => Err(LinearError::AuthConfig(
29            "LINEAR_API_TOKEN environment variable not set".to_string(),
30        )),
31    }
32}
33
34/// Resolve a Linear API token with precedence: env var -> file.
35/// (CLI flag takes highest precedence but is handled at the CLI layer.)
36pub fn auto_token() -> Result<String, LinearError> {
37    token_from_env().or_else(|_| token_from_file())
38}
39
40fn token_file_path() -> Result<PathBuf, LinearError> {
41    let home = home::home_dir()
42        .ok_or_else(|| LinearError::AuthConfig("Could not determine home directory".to_string()))?;
43    Ok(home.join(".linear_api_token"))
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49    use std::sync::Mutex;
50
51    /// Guards all tests that manipulate the `LINEAR_API_TOKEN` env var.
52    /// Tests run in parallel by default — without this, one test's `remove_var`
53    /// races with another test's `set_var`, causing spurious failures.
54    static ENV_LOCK: Mutex<()> = Mutex::new(());
55
56    /// Run a closure with `LINEAR_API_TOKEN` set to `value`, restoring the
57    /// original value (or removing it) when done — even on panic.
58    fn with_env_token<F: FnOnce()>(value: Option<&str>, f: F) {
59        let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
60        let original = std::env::var("LINEAR_API_TOKEN").ok();
61        match value {
62            Some(v) => std::env::set_var("LINEAR_API_TOKEN", v),
63            None => std::env::remove_var("LINEAR_API_TOKEN"),
64        }
65        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
66        match &original {
67            Some(v) => std::env::set_var("LINEAR_API_TOKEN", v),
68            None => std::env::remove_var("LINEAR_API_TOKEN"),
69        }
70        if let Err(e) = result {
71            std::panic::resume_unwind(e);
72        }
73    }
74
75    #[test]
76    fn token_from_env_success() {
77        with_env_token(Some("test-token-12345"), || {
78            assert_eq!(token_from_env().unwrap(), "test-token-12345");
79        });
80    }
81
82    #[test]
83    fn token_from_env_missing() {
84        with_env_token(None, || {
85            let result = token_from_env();
86            assert!(result.is_err());
87            assert!(result.unwrap_err().to_string().contains("LINEAR_API_TOKEN"));
88        });
89    }
90
91    #[test]
92    fn auto_token_prefers_env() {
93        with_env_token(Some("env-token-auto"), || {
94            assert_eq!(auto_token().unwrap(), "env-token-auto");
95        });
96    }
97
98    #[test]
99    fn token_from_env_empty_string_is_treated_as_absent() {
100        with_env_token(Some(""), || {
101            assert!(token_from_env().is_err());
102        });
103    }
104
105    #[test]
106    fn token_from_env_whitespace_only_is_treated_as_absent() {
107        with_env_token(Some("   "), || {
108            assert!(token_from_env().is_err());
109        });
110    }
111
112    #[test]
113    fn token_from_env_trims_whitespace() {
114        with_env_token(Some("  my-token  "), || {
115            assert_eq!(token_from_env().unwrap(), "my-token");
116        });
117    }
118
119    #[test]
120    fn token_file_path_is_home_based() {
121        let path = token_file_path().unwrap();
122        assert!(path.to_str().unwrap().contains(".linear_api_token"));
123        assert!(path.to_str().unwrap().starts_with("/"));
124    }
125}