use crate::state::types::{AdvisorySeverity, AurComment, NewsFeedSource};
use std::collections::HashMap;
use super::cache::{CACHE_TTL_SECONDS, CacheEntry, DiskCacheEntry, disk_cache_ttl_seconds};
use super::helpers::{
build_official_update_item, extract_date_from_pkg_json, normalize_pkg_date,
update_seen_for_comments,
};
use super::*;
#[test]
fn sort_news_items_orders_by_date_desc() {
let mut items = vec![
NewsFeedItem {
id: "1".into(),
date: "2024-01-02".into(),
title: "B".into(),
summary: None,
url: None,
source: NewsFeedSource::ArchNews,
severity: None,
packages: vec![],
},
NewsFeedItem {
id: "2".into(),
date: "2024-01-03".into(),
title: "A".into(),
summary: None,
url: None,
source: NewsFeedSource::ArchNews,
severity: None,
packages: vec![],
},
];
sort_news_items(&mut items, NewsSortMode::DateDesc);
assert_eq!(items.first().map(|i| &i.id), Some(&"2".to_string()));
}
#[test]
fn sort_news_items_orders_by_severity_then_date() {
let mut items = vec![
NewsFeedItem {
id: "c-critical".into(),
date: "2024-01-02".into(),
title: "crit".into(),
summary: None,
url: None,
source: NewsFeedSource::SecurityAdvisory,
severity: Some(AdvisorySeverity::Critical),
packages: vec![],
},
NewsFeedItem {
id: "d-medium".into(),
date: "2024-01-04".into(),
title: "med".into(),
summary: None,
url: None,
source: NewsFeedSource::SecurityAdvisory,
severity: Some(AdvisorySeverity::Medium),
packages: vec![],
},
NewsFeedItem {
id: "b-high-older".into(),
date: "2023-12-31".into(),
title: "high-old".into(),
summary: None,
url: None,
source: NewsFeedSource::SecurityAdvisory,
severity: Some(AdvisorySeverity::High),
packages: vec![],
},
NewsFeedItem {
id: "a-high-newer".into(),
date: "2024-01-03".into(),
title: "high-new".into(),
summary: None,
url: None,
source: NewsFeedSource::SecurityAdvisory,
severity: Some(AdvisorySeverity::High),
packages: vec![],
},
NewsFeedItem {
id: "e-unknown".into(),
date: "2024-01-05".into(),
title: "unknown".into(),
summary: None,
url: None,
source: NewsFeedSource::SecurityAdvisory,
severity: Some(AdvisorySeverity::Unknown),
packages: vec![],
},
NewsFeedItem {
id: "f-none".into(),
date: "2024-01-06".into(),
title: "none".into(),
summary: None,
url: None,
source: NewsFeedSource::ArchNews,
severity: None,
packages: vec![],
},
];
sort_news_items(&mut items, NewsSortMode::SeverityThenDate);
let ids: Vec<String> = items.into_iter().map(|i| i.id).collect();
assert_eq!(
ids,
vec![
"c-critical",
"a-high-newer",
"b-high-older",
"d-medium",
"e-unknown",
"f-none"
]
);
}
#[test]
fn update_seen_for_comments_emits_on_first_run() {
let mut seen = HashMap::new();
let comments = vec![AurComment {
id: Some("c1".into()),
author: "a".into(),
date: "2025-01-01 00:00 (UTC)".into(),
date_timestamp: Some(0),
date_url: Some("https://aur.archlinux.org/packages/foo#comment-1".into()),
content: "hello world".into(),
pinned: false,
}];
let emitted = update_seen_for_comments("foo", &comments, &mut seen, 5, true);
assert_eq!(emitted.len(), 1, "first run should emit newest comments");
assert_eq!(emitted[0].id, "aur-comment:foo:c1");
assert_eq!(seen.get("foo"), Some(&"c1".to_string()));
}
#[test]
fn update_seen_for_comments_emits_until_seen_marker() {
let mut seen = HashMap::from([("foo".to_string(), "c1".to_string())]);
let comments = vec![
AurComment {
id: Some("c2".into()),
author: "a".into(),
date: "2025-01-02 00:00 (UTC)".into(),
date_timestamp: Some(0),
date_url: Some("https://aur.archlinux.org/packages/foo#comment-2".into()),
content: "second".into(),
pinned: false,
},
AurComment {
id: Some("c1".into()),
author: "a".into(),
date: "2025-01-01 00:00 (UTC)".into(),
date_timestamp: Some(0),
date_url: Some("https://aur.archlinux.org/packages/foo#comment-1".into()),
content: "first".into(),
pinned: false,
},
];
let emitted = update_seen_for_comments("foo", &comments, &mut seen, 5, false);
assert_eq!(emitted.len(), 1);
assert_eq!(emitted[0].id, "aur-comment:foo:c2");
assert_eq!(seen.get("foo"), Some(&"c2".to_string()));
}
#[test]
fn normalize_pkg_date_handles_rfc3339_and_utc_formats() {
assert_eq!(
normalize_pkg_date("2025-12-07T11:09:38Z"),
Some("2025-12-07".to_string())
);
assert_eq!(
normalize_pkg_date("2025-12-07 11:09 UTC"),
Some("2025-12-07".to_string())
);
assert_eq!(
normalize_pkg_date("2025-12-15T19:30:14.422Z"),
Some("2025-12-15".to_string())
);
}
#[test]
fn extract_date_from_pkg_json_prefers_last_update() {
let val = serde_json::json!({
"pkg": {
"last_update": "2025-12-07T11:09:38Z",
"build_date": "2024-01-01T00:00:00Z"
}
});
let Some(pkg) = val.get("pkg") else {
panic!("pkg key missing");
};
let date = extract_date_from_pkg_json(pkg);
assert_eq!(date, Some("2025-12-07".to_string()));
}
#[test]
fn build_official_update_item_uses_metadata_date_when_available() {
let pkg = crate::state::PackageItem {
name: "xterm".into(),
version: "1".into(),
description: "term".into(),
source: crate::state::Source::Official {
repo: "extra".into(),
arch: "x86_64".into(),
},
popularity: None,
out_of_date: None,
orphaned: false,
};
let item = build_official_update_item(&pkg, None, Some("1"), "2", Some("2025-12-07".into()));
assert_eq!(item.date, "2025-12-07");
}
#[test]
fn test_load_from_disk_cache_handles_ttl_and_corruption() {
use std::fs;
use tempfile::TempDir;
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let cache_path = temp_dir.path().join("arch_news_cache.json");
let valid_entry = DiskCacheEntry {
data: vec![NewsFeedItem {
id: "test-1".into(),
date: "2025-01-01".into(),
title: "Test News".into(),
summary: None,
url: Some("https://example.com".into()),
source: NewsFeedSource::ArchNews,
severity: None,
packages: Vec::new(),
}],
saved_at: chrono::Utc::now().timestamp(),
};
fs::write(
&cache_path,
serde_json::to_string(&valid_entry).expect("Failed to serialize"),
)
.expect("Failed to write cache file");
let content = fs::read_to_string(&cache_path).expect("Failed to read cache file");
let entry: DiskCacheEntry = serde_json::from_str(&content).expect("Failed to parse cache");
let now = chrono::Utc::now().timestamp();
let age = now - entry.saved_at;
let ttl = disk_cache_ttl_seconds();
assert!(age < ttl, "Valid cache should not be expired");
let expired_entry = DiskCacheEntry {
data: vec![],
saved_at: chrono::Utc::now().timestamp() - (ttl + 86400), };
fs::write(
&cache_path,
serde_json::to_string(&expired_entry).expect("Failed to serialize"),
)
.expect("Failed to write cache file");
let content = fs::read_to_string(&cache_path).expect("Failed to read cache file");
let entry: DiskCacheEntry = serde_json::from_str(&content).expect("Failed to parse cache");
let age = now - entry.saved_at;
assert!(age >= ttl, "Expired cache should be detected");
fs::write(&cache_path, "invalid json{").expect("Failed to write corrupted cache");
assert!(
serde_json::from_str::<DiskCacheEntry>(
&fs::read_to_string(&cache_path).expect("Failed to read corrupted cache")
)
.is_err()
);
}
#[test]
fn test_in_memory_cache_ttl() {
use std::collections::HashMap;
use std::time::{Duration, Instant};
let mut cache: HashMap<String, CacheEntry> = HashMap::new();
let now = Instant::now();
cache.insert(
"arch_news".to_string(),
CacheEntry {
data: vec![NewsFeedItem {
id: "test-1".into(),
date: "2025-01-01".into(),
title: "Test".into(),
summary: None,
url: Some("https://example.com".into()),
source: NewsFeedSource::ArchNews,
severity: None,
packages: Vec::new(),
}],
timestamp: now,
},
);
if let Some(entry) = cache.get("arch_news") {
let elapsed = entry.timestamp.elapsed().as_secs();
assert!(
elapsed < CACHE_TTL_SECONDS,
"Recent entry should be within TTL"
);
}
let old_timestamp = now
.checked_sub(Duration::from_secs(CACHE_TTL_SECONDS + 1))
.expect("Timestamp subtraction should not overflow");
cache.insert(
"arch_news".to_string(),
CacheEntry {
data: vec![],
timestamp: old_timestamp,
},
);
if let Some(entry) = cache.get("arch_news") {
let elapsed = entry.timestamp.elapsed().as_secs();
assert!(elapsed >= CACHE_TTL_SECONDS, "Old entry should be expired");
}
}
#[test]
fn test_disk_cache_save_and_load() {
use std::fs;
use tempfile::TempDir;
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let cache_path = temp_dir.path().join("arch_news_cache.json");
let items = vec![NewsFeedItem {
id: "test-1".into(),
date: "2025-01-01".into(),
title: "Test News".into(),
summary: None,
url: Some("https://example.com".into()),
source: NewsFeedSource::ArchNews,
severity: None,
packages: Vec::new(),
}];
let entry = DiskCacheEntry {
data: items.clone(),
saved_at: chrono::Utc::now().timestamp(),
};
fs::write(
&cache_path,
serde_json::to_string(&entry).expect("Failed to serialize"),
)
.expect("Failed to write cache file");
let content = fs::read_to_string(&cache_path).expect("Failed to read cache file");
let loaded_entry: DiskCacheEntry =
serde_json::from_str(&content).expect("Failed to parse cache");
assert_eq!(loaded_entry.data.len(), items.len());
assert_eq!(loaded_entry.data[0].id, items[0].id);
}
#[test]
fn test_cutoff_date_disables_caching() {
let cutoff_date = Some("2025-01-01");
assert!(cutoff_date.is_some(), "cutoff_date should disable caching");
}
#[test]
fn test_news_feed_context_toggles() {
use std::collections::HashSet;
let mut seen_versions = HashMap::new();
let mut seen_comments = HashMap::new();
let installed = HashSet::new();
let ctx = NewsFeedContext {
force_emit_all: false,
updates_list_path: None,
limit: 10,
include_arch_news: true,
include_advisories: false,
include_pkg_updates: false,
include_aur_comments: false,
installed_filter: Some(&installed),
installed_only: false,
sort_mode: NewsSortMode::DateDesc,
seen_pkg_versions: &mut seen_versions,
seen_aur_comments: &mut seen_comments,
max_age_days: None,
};
assert!(ctx.include_arch_news);
assert!(!ctx.include_advisories);
assert!(!ctx.include_pkg_updates);
assert!(!ctx.include_aur_comments);
}
#[test]
fn test_max_age_cutoff_date_calculation() {
let max_age_days = Some(7u32);
let cutoff_date = max_age_days.and_then(|days| {
chrono::Utc::now()
.checked_sub_signed(chrono::Duration::days(i64::from(days)))
.map(|dt| dt.format("%Y-%m-%d").to_string())
});
let cutoff = cutoff_date.expect("cutoff_date should be Some");
assert_eq!(cutoff.len(), 10);
assert!(cutoff.contains('-'));
}
#[test]
fn test_seen_map_updates_for_comments() {
let mut seen = HashMap::new();
let comments = vec![
AurComment {
id: Some("c2".into()),
author: "a".into(),
date: "2025-01-02 00:00 (UTC)".into(),
date_timestamp: Some(0),
date_url: Some("https://aur.archlinux.org/packages/foo#comment-2".into()),
content: "second".into(),
pinned: false,
},
AurComment {
id: Some("c1".into()),
author: "a".into(),
date: "2025-01-01 00:00 (UTC)".into(),
date_timestamp: Some(0),
date_url: Some("https://aur.archlinux.org/packages/foo#comment-1".into()),
content: "first".into(),
pinned: false,
},
];
let _emitted = update_seen_for_comments("foo", &comments, &mut seen, 5, false);
assert_eq!(seen.get("foo"), Some(&"c2".to_string()));
}