1use crate::error::LinearError;
7use std::path::PathBuf;
8
9pub 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
24pub 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
34pub 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 static ENV_LOCK: Mutex<()> = Mutex::new(());
55
56 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}