fix: resolve CI workflow path and add lint job
Some checks failed
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled

This commit is contained in:
2026-01-31 23:08:36 +00:00
parent 0fbb5d7418
commit 0c08361463

215
src/rotation.rs Normal file
View File

@@ -0,0 +1,215 @@
use crate::vault::Vault;
use crate::token::TokenData;
use chrono::{DateTime, Utc, Duration};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct RotationSchedule {
pub token_name: String,
pub rotation_days: u32,
pub last_rotated: Option<DateTime<Utc>>,
pub next_rotation: Option<DateTime<Utc>>,
}
impl RotationSchedule {
pub fn from_token(token_name: &str, token_data: &TokenData) -> Option<Self> {
if !token_data.auto_rotate {
return None;
}
let last_rotated = token_data.last_rotated.unwrap_or(token_data.created_at);
let next_rotation = Some(last_rotated + Duration::days(token_data.rotation_days.unwrap_or(30) as i64));
Some(RotationSchedule {
token_name: token_name.to_string(),
rotation_days: token_data.rotation_days.unwrap_or(30),
last_rotated: Some(last_rotated),
next_rotation,
})
}
pub fn is_due(&self) -> bool {
if let Some(next) = self.next_rotation {
Utc::now() >= next
} else {
false
}
}
pub fn days_until_rotation(&self) -> Option<i64> {
self.next_rotation.map(|next| {
let diff = next - Utc::now();
diff.num_days()
})
}
}
pub struct RotationEngine;
impl RotationEngine {
pub fn check_schedules(vault: &Vault) -> Vec<RotationSchedule> {
let mut schedules = Vec::new();
for (name, token_data) in &vault.tokens {
if let Some(schedule) = RotationSchedule::from_token(name, token_data) {
schedules.push(schedule);
}
}
schedules
}
pub fn get_due_rotations(vault: &Vault) -> Vec<RotationSchedule> {
Self::check_schedules(vault)
.into_iter()
.filter(|s| s.is_due())
.collect()
}
pub fn get_upcoming_rotations(vault: &Vault, days: i64) -> Vec<RotationSchedule> {
let now = Utc::now();
let threshold = now + Duration::days(days);
Self::check_schedules(vault)
.into_iter()
.filter(|s| {
if let Some(next) = s.next_rotation {
next >= now && next <= threshold
} else {
false
}
})
.collect()
}
pub fn rotate_due_tokens(vault: &mut Vault) -> HashMap<String, String> {
let due_names: Vec<String> = vault.tokens.iter()
.filter(|(_, token)| token.should_rotate())
.map(|(name, _)| name.clone())
.collect();
let mut results = HashMap::new();
for name in due_names {
if let Ok(new_value) = vault.rotate_token(&name, true) {
results.insert(name, new_value);
}
}
results
}
pub fn calculate_next_rotation(token_data: &TokenData) -> Option<DateTime<Utc>> {
let base_time = token_data.last_rotated.unwrap_or(token_data.created_at);
let days = token_data.rotation_days.unwrap_or(30);
Some(base_time + Duration::days(days as i64))
}
pub fn format_schedule_report(schedules: &[RotationSchedule]) -> String {
if schedules.is_empty() {
return "No scheduled rotations".to_string();
}
let mut report = String::from("Rotation Schedule:\n");
report.push_str(&"=".repeat(50));
report.push('\n');
let mut due_count = 0;
let mut upcoming_count = 0;
for schedule in schedules {
let status = if schedule.is_due() {
due_count += 1;
"DUE NOW"
} else if let Some(days) = schedule.days_until_rotation() {
upcoming_count += 1;
format!("in {} days", days)
} else {
"unknown".to_string()
};
report.push_str(&format!(
" {}: every {} days - {}\n",
schedule.token_name,
schedule.rotation_days,
status
));
}
report.push_str(&"-".repeat(50));
report.push('\n');
report.push_str(&format!("Due now: {}, Upcoming: {}\n", due_count, upcoming_count));
report
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vault::Vault;
#[test]
fn test_rotation_schedule_creation() {
let password = "test";
let project = "test_schedule";
let mut vault = Vault::initialize(password, project).unwrap();
vault.generate_token("auto_rotate", 32).unwrap();
vault.set_rotation("auto_rotate", 30).unwrap();
let schedules = RotationEngine::check_schedules(&vault);
assert_eq!(schedules.len(), 1);
assert_eq!(schedules[0].token_name, "auto_rotate");
assert_eq!(schedules[0].rotation_days, 30);
let vault_path = Vault::get_vault_path(project).unwrap();
if vault_path.exists() {
std::fs::remove_file(&vault_path).unwrap();
}
}
#[test]
fn test_manual_rotation_not_scheduled() {
let password = "test";
let project = "test_manual";
let mut vault = Vault::initialize(password, project).unwrap();
vault.generate_token("manual", 32).unwrap();
let schedules = RotationEngine::check_schedules(&vault);
assert!(schedules.is_empty());
let vault_path = Vault::get_vault_path(project).unwrap();
if vault_path.exists() {
std::fs::remove_file(&vault_path).unwrap();
}
}
#[test]
fn test_schedule_report() {
let schedules = vec![
RotationSchedule {
token_name: "token1".to_string(),
rotation_days: 30,
last_rotated: Some(Utc::now() - Duration::days(35)),
next_rotation: Some(Utc::now() - Duration::days(5)),
},
RotationSchedule {
token_name: "token2".to_string(),
rotation_days: 60,
last_rotated: Some(Utc::now()),
next_rotation: Some(Utc::now() + Duration::days(60)),
},
];
let report = RotationEngine::format_schedule_report(&schedules);
assert!(report.contains("token1"));
assert!(report.contains("token2"));
assert!(report.contains("DUE NOW"));
assert!(report.contains("in 60 days"));
let empty_report = RotationEngine::format_schedule_report(&[]);
assert_eq!(empty_report, "No scheduled rotations");
}
}