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}