use crate::{
color,
config::{self, Config},
errors::Error,
oauth,
};
use clap::{Parser, Subcommand};
use std::{io::ErrorKind, path::PathBuf};
#[derive(Subcommand, Debug, Clone)]
pub enum AuthCommands {
#[clap(alias = "l")]
Login(Login),
#[clap(alias = "t")]
Token(Token),
}
#[derive(Parser, Debug, Clone)]
pub struct Login {}
#[derive(Parser, Debug, Clone)]
pub struct Token {
key: String,
}
pub async fn login(config: &mut Config, _args: &Login) -> Result<String, Error> {
oauth::login(config, None).await
}
pub async fn token(config_path: Option<PathBuf>, args: &Token) -> Result<String, Error> {
let Token { key } = args;
let trimmed_key = key.trim();
if trimmed_key.is_empty() {
return Err(Error::new(
"auth token",
"Todoist API token cannot be empty or whitespace",
));
}
let path = config::resolve_config_path(config_path).await?;
let mut config = match tokio::fs::metadata(&path).await {
Err(e) if e.kind() == ErrorKind::NotFound => {
let config = Config::new(None, path.clone()).await?;
config.touch_file().await?;
config
}
Err(e) => return Err(e.into()),
Ok(_) => Config::load(&path).await?,
};
config.set_token(trimmed_key.to_string()).await?;
Ok(color::green_string(&format!(
"✓ API token saved to {}",
path.display()
)))
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[tokio::test]
async fn test_token_creates_config_and_saves_token() {
let dir = tempdir().expect("temp dir should be created");
let path = dir.path().join("tod.cfg");
let args = Token {
key: "test-api-token-123".to_string(),
};
let result = token(Some(path.clone()), &args)
.await
.expect("token command should succeed");
assert!(result.contains("✓"), "should return success checkmark");
let config = Config::load(&path)
.await
.expect("config should be readable after token command");
assert_eq!(
config.token,
Some("test-api-token-123".to_string()),
"token should be saved in config"
);
}
#[tokio::test]
async fn test_token_updates_existing_config_token() {
let dir = tempdir().expect("temp dir should be created");
let path = dir.path().join("tod.cfg");
let initial = Token {
key: "old-token".to_string(),
};
token(Some(path.clone()), &initial)
.await
.expect("first token call should succeed");
let updated = Token {
key: "new-token".to_string(),
};
token(Some(path.clone()), &updated)
.await
.expect("second token call should succeed");
let config = Config::load(&path)
.await
.expect("config should be readable");
assert_eq!(
config.token,
Some("new-token".to_string()),
"token should be updated in config"
);
}
#[tokio::test]
async fn test_token_rejects_empty_or_whitespace_key() {
let dir = tempdir().expect("temp dir should be created");
let path = dir.path().join("tod.cfg");
let empty = Token {
key: " ".to_string(),
};
let error = token(Some(path), &empty)
.await
.expect_err("empty token should be rejected");
assert_eq!(error.source, "auth token");
assert!(
error.message.contains("cannot be empty or whitespace"),
"error message should explain empty/whitespace rejection"
);
}
}