fitbit_rs/access_token.rs
1//! Functionality for retrieving Fitbit API access tokens.
2//!
3//! This module provides functions to read the Fitbit access token from a configuration file
4//! located in the user's home directory.
5
6use ini::Ini;
7use std::path::PathBuf;
8use thiserror::Error;
9
10/// Errors that can occur when retrieving an access token
11#[derive(Error, Debug)]
12pub enum AccessTokenError {
13 /// The user's home directory could not be determined
14 #[error("Home directory not found")]
15 HomeDirectoryNotFound,
16
17 /// Failed to load or parse the configuration file
18 #[error("Failed to load config file: {0}")]
19 ConfigLoadError(#[from] ini::Error),
20
21 /// The access token was not found in the configuration file
22 #[error("ACCESS_TOKEN not found in config.ini")]
23 AccessTokenNotFound,
24
25 /// The configuration directory or file could not be created
26 #[error("Failed to create config directory or file: {0}")]
27 ConfigCreationError(std::io::Error),
28}
29
30/// Returns the path to the configuration file
31///
32/// The configuration file is located at `~/.config/fitbit-rs/config.ini`.
33pub fn get_config_path() -> Result<PathBuf, AccessTokenError> {
34 dirs::home_dir()
35 .ok_or(AccessTokenError::HomeDirectoryNotFound)
36 .map(|home| home.join(".config").join("fitbit-rs").join("config.ini"))
37}
38
39/// Retrieves the Fitbit API access token from the configuration file
40///
41/// The access token is expected to be stored in the `[Fitbit]` section under the key
42/// `ACCESS_TOKEN` in the configuration file at `~/.config/lifestats/config.ini`.
43///
44/// # Returns
45///
46/// The access token as a string if found, otherwise an error.
47///
48/// # Errors
49///
50/// Returns an error if:
51/// - The home directory could not be determined
52/// - The configuration file could not be loaded or parsed
53/// - The access token was not found in the configuration file
54///
55/// # Example
56///
57/// ```no_run
58/// use fitbit_rs::access_token::get_access_token;
59///
60/// match get_access_token() {
61/// Ok(token) => println!("Found access token: {}", token),
62/// Err(err) => eprintln!("Error getting access token: {}", err),
63/// }
64/// ```
65pub fn get_access_token() -> Result<String, AccessTokenError> {
66 let config_path = get_config_path()?;
67
68 // Load the config if it exists
69 let config = Ini::load_from_file(&config_path)?;
70
71 // Retrieve the access token
72 config
73 .get_from(Some("Fitbit"), "ACCESS_TOKEN")
74 .ok_or(AccessTokenError::AccessTokenNotFound)
75 .map(String::from)
76}
77
78/// Stores a Fitbit API access token in the configuration file
79///
80/// Creates the configuration file and directory if they don't exist.
81///
82/// # Arguments
83///
84/// * `access_token` - The access token to store
85///
86/// # Returns
87///
88/// `Ok(())` if successful, otherwise an error.
89///
90/// # Errors
91///
92/// Returns an error if:
93/// - The home directory could not be determined
94/// - The configuration directory could not be created
95/// - The configuration file could not be written
96pub fn store_access_token(access_token: &str) -> Result<(), AccessTokenError> {
97 let config_path = get_config_path()?;
98
99 // Create parent directories if they don't exist
100 if let Some(parent) = config_path.parent() {
101 std::fs::create_dir_all(parent).map_err(AccessTokenError::ConfigCreationError)?;
102 }
103
104 // Load existing config or create a new one
105 let mut config = Ini::load_from_file(&config_path).unwrap_or_else(|_| Ini::new());
106
107 // Set the access token
108 config
109 .with_section(Some("Fitbit"))
110 .set("ACCESS_TOKEN", access_token);
111
112 // Save the config
113 config
114 .write_to_file(&config_path)
115 .map_err(AccessTokenError::ConfigCreationError)?;
116
117 Ok(())
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use std::env;
124 use std::fs;
125 use tempfile::tempdir;
126
127 #[test]
128 fn test_store_and_retrieve_access_token() {
129 // TODO: Make the original code more testable
130 // Create a temporary directory for testing
131 let temp_dir = tempdir().unwrap();
132 let config_dir = temp_dir.path().join(".config").join("lifestats");
133 fs::create_dir_all(&config_dir).unwrap();
134
135 // Mock the home directory
136 let original_home = env::var("HOME").ok();
137 unsafe {
138 env::set_var("HOME", temp_dir.path());
139 }
140
141 // Test token
142 let test_token = "test_access_token_123";
143
144 // Store the token
145 store_access_token(test_token).unwrap();
146
147 // Retrieve the token
148 let retrieved_token = get_access_token().unwrap();
149
150 // Verify
151 assert_eq!(retrieved_token, test_token);
152
153 // Restore original home directory
154 if let Some(home) = original_home {
155 unsafe {
156 env::set_var("HOME", home);
157 }
158 } else {
159 unsafe {
160 env::remove_var("HOME");
161 }
162 }
163 }
164}