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