1use crate::error::LinearError;
7use std::path::Path;
8
9pub 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
28pub 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 static ENV_LOCK: Mutex<()> = Mutex::new(());
47
48 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}