omni_dev/atlassian/
auth.rs1use std::collections::HashMap;
7use std::fs;
8
9use anyhow::{Context, Result};
10
11use crate::atlassian::error::AtlassianError;
12use crate::utils::settings::Settings;
13
14pub const ATLASSIAN_INSTANCE_URL: &str = "ATLASSIAN_INSTANCE_URL";
16
17pub const ATLASSIAN_EMAIL: &str = "ATLASSIAN_EMAIL";
19
20pub const ATLASSIAN_API_TOKEN: &str = "ATLASSIAN_API_TOKEN";
22
23#[derive(Debug, Clone)]
25pub struct AtlassianCredentials {
26 pub instance_url: String,
28
29 pub email: String,
31
32 pub api_token: String,
34}
35
36pub fn load_credentials() -> Result<AtlassianCredentials> {
40 let settings = Settings::load().unwrap_or(Settings {
41 env: HashMap::new(),
42 });
43
44 let instance_url = settings
45 .get_env_var(ATLASSIAN_INSTANCE_URL)
46 .ok_or(AtlassianError::CredentialsNotFound)?;
47 let email = settings
48 .get_env_var(ATLASSIAN_EMAIL)
49 .ok_or(AtlassianError::CredentialsNotFound)?;
50 let api_token = settings
51 .get_env_var(ATLASSIAN_API_TOKEN)
52 .ok_or(AtlassianError::CredentialsNotFound)?;
53
54 let instance_url = instance_url.trim_end_matches('/').to_string();
56
57 Ok(AtlassianCredentials {
58 instance_url,
59 email,
60 api_token,
61 })
62}
63
64pub fn save_credentials(credentials: &AtlassianCredentials) -> Result<()> {
69 let settings_path = Settings::get_settings_path()?;
70
71 let mut settings_value: serde_json::Value = if settings_path.exists() {
73 let content = fs::read_to_string(&settings_path)
74 .with_context(|| format!("Failed to read {}", settings_path.display()))?;
75 serde_json::from_str(&content)
76 .with_context(|| format!("Failed to parse {}", settings_path.display()))?
77 } else {
78 serde_json::json!({})
79 };
80
81 if !settings_value
83 .get("env")
84 .is_some_and(serde_json::Value::is_object)
85 {
86 settings_value["env"] = serde_json::json!({});
87 }
88
89 let Some(env) = settings_value["env"].as_object_mut() else {
91 anyhow::bail!("Internal error: env key is not an object after initialization");
92 };
93 env.insert(
94 ATLASSIAN_INSTANCE_URL.to_string(),
95 serde_json::Value::String(credentials.instance_url.clone()),
96 );
97 env.insert(
98 ATLASSIAN_EMAIL.to_string(),
99 serde_json::Value::String(credentials.email.clone()),
100 );
101 env.insert(
102 ATLASSIAN_API_TOKEN.to_string(),
103 serde_json::Value::String(credentials.api_token.clone()),
104 );
105
106 if let Some(parent) = settings_path.parent() {
108 fs::create_dir_all(parent)
109 .with_context(|| format!("Failed to create directory {}", parent.display()))?;
110 }
111
112 let formatted = serde_json::to_string_pretty(&settings_value)
114 .context("Failed to serialize settings JSON")?;
115 fs::write(&settings_path, formatted)
116 .with_context(|| format!("Failed to write {}", settings_path.display()))?;
117
118 Ok(())
119}
120
121#[cfg(test)]
122#[allow(clippy::unwrap_used, clippy::expect_used)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn save_and_read_credentials() {
128 let temp_dir = {
129 std::fs::create_dir_all("tmp").ok();
130 tempfile::TempDir::new_in("tmp").unwrap()
131 };
132 let settings_path = temp_dir.path().join("settings.json");
133
134 let existing = r#"{"env": {"SOME_KEY": "value"}}"#;
136 fs::write(&settings_path, existing).unwrap();
137
138 let content = fs::read_to_string(&settings_path).unwrap();
140 let mut val: serde_json::Value = serde_json::from_str(&content).unwrap();
141 val["env"]["ATLASSIAN_INSTANCE_URL"] =
142 serde_json::Value::String("https://test.atlassian.net".to_string());
143 val["env"]["ATLASSIAN_EMAIL"] = serde_json::Value::String("user@example.com".to_string());
144 val["env"]["ATLASSIAN_API_TOKEN"] = serde_json::Value::String("secret-token".to_string());
145 let formatted = serde_json::to_string_pretty(&val).unwrap();
146 fs::write(&settings_path, formatted).unwrap();
147
148 let content = fs::read_to_string(&settings_path).unwrap();
150 let val: serde_json::Value = serde_json::from_str(&content).unwrap();
151 assert_eq!(val["env"]["SOME_KEY"], "value");
152 assert_eq!(
153 val["env"]["ATLASSIAN_INSTANCE_URL"],
154 "https://test.atlassian.net"
155 );
156 assert_eq!(val["env"]["ATLASSIAN_EMAIL"], "user@example.com");
157 assert_eq!(val["env"]["ATLASSIAN_API_TOKEN"], "secret-token");
158 }
159
160 #[test]
161 fn load_credentials_normalizes_trailing_slash() {
162 let url = "https://env.atlassian.net/";
164 let normalized = url.trim_end_matches('/').to_string();
165 assert_eq!(normalized, "https://env.atlassian.net");
166 }
167
168 #[test]
169 fn constant_key_names() {
170 assert_eq!(ATLASSIAN_INSTANCE_URL, "ATLASSIAN_INSTANCE_URL");
171 assert_eq!(ATLASSIAN_EMAIL, "ATLASSIAN_EMAIL");
172 assert_eq!(ATLASSIAN_API_TOKEN, "ATLASSIAN_API_TOKEN");
173 }
174
175 #[test]
176 fn credentials_struct_clone_and_debug() {
177 let creds = AtlassianCredentials {
178 instance_url: "https://org.atlassian.net".to_string(),
179 email: "user@test.com".to_string(),
180 api_token: "token".to_string(),
181 };
182 let cloned = creds.clone();
183 assert_eq!(cloned.instance_url, creds.instance_url);
184 assert_eq!(cloned.email, creds.email);
185 assert_eq!(cloned.api_token, creds.api_token);
186 let debug = format!("{creds:?}");
188 assert!(debug.contains("AtlassianCredentials"));
189 }
190
191 #[test]
194 fn save_credentials_creates_and_preserves() {
195 use std::sync::Mutex;
196 static HOME_MUTEX: Mutex<()> = Mutex::new(());
197 let _lock = HOME_MUTEX.lock().unwrap();
198
199 let original_home = std::env::var("HOME").ok();
200
201 {
203 let temp_dir = {
204 std::fs::create_dir_all("tmp").ok();
205 tempfile::TempDir::new_in("tmp").unwrap()
206 };
207 std::env::set_var("HOME", temp_dir.path());
208
209 let creds = AtlassianCredentials {
210 instance_url: "https://save.atlassian.net".to_string(),
211 email: "save@example.com".to_string(),
212 api_token: "save-token".to_string(),
213 };
214 save_credentials(&creds).unwrap();
215
216 let settings_path = temp_dir.path().join(".omni-dev").join("settings.json");
217 assert!(settings_path.exists());
218 let content = fs::read_to_string(&settings_path).unwrap();
219 let val: serde_json::Value = serde_json::from_str(&content).unwrap();
220 assert_eq!(
221 val["env"]["ATLASSIAN_INSTANCE_URL"],
222 "https://save.atlassian.net"
223 );
224 assert_eq!(val["env"]["ATLASSIAN_EMAIL"], "save@example.com");
225 assert_eq!(val["env"]["ATLASSIAN_API_TOKEN"], "save-token");
226 }
227
228 {
230 let temp_dir = {
231 std::fs::create_dir_all("tmp").ok();
232 tempfile::TempDir::new_in("tmp").unwrap()
233 };
234 let omni_dir = temp_dir.path().join(".omni-dev");
235 fs::create_dir_all(&omni_dir).unwrap();
236 let settings_path = omni_dir.join("settings.json");
237 fs::write(
238 &settings_path,
239 r#"{"env": {"OTHER_KEY": "keep_me"}, "extra": true}"#,
240 )
241 .unwrap();
242
243 std::env::set_var("HOME", temp_dir.path());
244
245 let creds = AtlassianCredentials {
246 instance_url: "https://org.atlassian.net".to_string(),
247 email: "user@test.com".to_string(),
248 api_token: "token".to_string(),
249 };
250 save_credentials(&creds).unwrap();
251
252 let content = fs::read_to_string(&settings_path).unwrap();
253 let val: serde_json::Value = serde_json::from_str(&content).unwrap();
254 assert_eq!(val["env"]["OTHER_KEY"], "keep_me");
255 assert_eq!(val["extra"], true);
256 assert_eq!(
257 val["env"]["ATLASSIAN_INSTANCE_URL"],
258 "https://org.atlassian.net"
259 );
260 }
261
262 if let Some(home) = original_home {
264 std::env::set_var("HOME", home);
265 }
266 }
267}