use crate::sources::news::parse::{parse_arch_news_html, prune_news_boilerplate};
use crate::sources::news::utils::{extract_between, strip_time_and_tz};
#[test]
fn advisory_boilerplate_is_removed() {
let input = r"
Arch Linux
• Home
• Packages
Arch Linux Security Advisory ASA-202506-6 =========================================
Severity: Low
Date : 2025-06-12
Summary =======
The package python-django before version 5.1.11-1 is vulnerable to content spoofing.
";
let pruned = prune_news_boilerplate(input);
assert!(pruned.starts_with("Arch Linux Security Advisory"));
assert!(pruned.contains("Severity: Low"));
assert!(!pruned.contains("Home"));
assert!(!pruned.contains("Packages"));
}
#[test]
fn advisory_html_strips_links_and_keeps_text() {
let html = r#"
<div class="advisory">
Arch Linux Security Advisory ASA-202506-6 =========================================
Severity: Low
Package : <a href="/package/konsolen">konsolen</a>
Link : <a href="https://security.archlinux.org/AVG-2897">https://security.archlinux.org/AVG-2897</a>
Summary =======
The package before version 25.04.2-1 is vulnerable to arbitrary code execution.
Resolution =========
Upgrade to 25.04.2-1.
Description ===========
has a path where if telnet was not available it would fall back to using bash for the given arguments provided; this allows an attacker to execute arbitrary code.
</div>
"#;
let parsed = parse_arch_news_html(html, None);
assert!(parsed.contains("Arch Linux Security Advisory"));
assert!(parsed.contains("Severity: Low"));
assert!(parsed.contains("Package : konsolen"));
assert!(parsed.contains("https://security.archlinux.org/AVG-2897"));
assert!(!parsed.contains("<a href"));
}
#[test]
fn news_extract_between_and_strip_time_tz() {
assert_eq!(
extract_between("<a>hi</a>", "<a>", "</a>")
.expect("extract_between should find 'hi' in test string"),
"hi"
);
assert!(extract_between("nope", "<a>", "</a>").is_none());
assert_eq!(
strip_time_and_tz("Mon, 23 Oct 2023 12:34:56 +0000"),
"2023-10-23"
);
assert_eq!(strip_time_and_tz("Mon, 23 Oct 2023 12:34:56"), "2023-10-23");
assert_eq!(strip_time_and_tz("Mon, 23 Oct 2023"), "2023-10-23");
assert_eq!(strip_time_and_tz("2023-10-23"), "2023-10-23");
assert_eq!(strip_time_and_tz("Thu, 21 Aug 2025"), "2025-08-21");
}
#[test]
fn test_fetch_arch_news_respects_limit() {
let rss = r#"<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<item>
<title>Item 1</title>
<link>https://archlinux.org/news/item-1/</link>
<pubDate>Mon, 01 Jan 2025 12:00:00 +0000</pubDate>
</item>
<item>
<title>Item 2</title>
<link>https://archlinux.org/news/item-2/</link>
<pubDate>Mon, 02 Jan 2025 12:00:00 +0000</pubDate>
</item>
<item>
<title>Item 3</title>
<link>https://archlinux.org/news/item-3/</link>
<pubDate>Mon, 03 Jan 2025 12:00:00 +0000</pubDate>
</item>
</channel>
</rss>"#;
let mut items = Vec::new();
let mut pos = 0;
let limit = 2;
while items.len() < limit {
if let Some(start) = rss[pos..].find("<item>") {
let s = pos + start;
let end = rss[s..].find("</item>").map_or(rss.len(), |e| s + e + 7);
let chunk = &rss[s..end];
let title = extract_between(chunk, "<title>", "</title>").unwrap_or_default();
let link = extract_between(chunk, "<link>", "</link>").unwrap_or_default();
let raw_date = extract_between(chunk, "<pubDate>", "</pubDate>")
.map(|d| d.trim().to_string())
.unwrap_or_default();
let date = strip_time_and_tz(&raw_date);
items.push(crate::state::NewsItem {
date,
title,
url: link,
});
pos = end;
} else {
break;
}
}
assert_eq!(items.len(), 2);
assert_eq!(items[0].title, "Item 1");
assert_eq!(items[1].title, "Item 2");
}
#[test]
fn test_fetch_arch_news_handles_missing_tags() {
let rss = r#"<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<item>
<title>Item with missing link</title>
<pubDate>Mon, 01 Jan 2025 12:00:00 +0000</pubDate>
</item>
<item>
<title>Item with missing date</title>
<link>https://archlinux.org/news/missing-date/</link>
</item>
<item>
<link>https://archlinux.org/news/missing-title/</link>
<pubDate>Mon, 01 Jan 2025 12:00:00 +0000</pubDate>
</item>
</channel>
</rss>"#;
let mut items = Vec::new();
let mut pos = 0;
let limit = 10;
while items.len() < limit {
if let Some(start) = rss[pos..].find("<item>") {
let s = pos + start;
let end = rss[s..].find("</item>").map_or(rss.len(), |e| s + e + 7);
let chunk = &rss[s..end];
let title = extract_between(chunk, "<title>", "</title>").unwrap_or_default();
let link = extract_between(chunk, "<link>", "</link>").unwrap_or_default();
let raw_date = extract_between(chunk, "<pubDate>", "</pubDate>")
.map(|d| d.trim().to_string())
.unwrap_or_default();
let date = strip_time_and_tz(&raw_date);
items.push(crate::state::NewsItem {
date,
title,
url: link,
});
pos = end;
} else {
break;
}
}
assert_eq!(items.len(), 3);
assert_eq!(items[0].title, "Item with missing link");
assert_eq!(items[0].url, "");
assert_eq!(items[1].title, "Item with missing date");
assert_eq!(items[1].date, "");
assert_eq!(items[2].title, "");
assert_eq!(items[2].url, "https://archlinux.org/news/missing-title/");
}
#[test]
fn test_fetch_arch_news_respects_cutoff_date() {
let rss = r#"<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<item>
<title>Item 1</title>
<link>https://archlinux.org/news/item-1/</link>
<pubDate>Mon, 01 Jan 2025 12:00:00 +0000</pubDate>
</item>
<item>
<title>Item 2</title>
<link>https://archlinux.org/news/item-2/</link>
<pubDate>Mon, 02 Jan 2025 12:00:00 +0000</pubDate>
</item>
<item>
<title>Item 3</title>
<link>https://archlinux.org/news/item-3/</link>
<pubDate>Mon, 03 Jan 2025 12:00:00 +0000</pubDate>
</item>
</channel>
</rss>"#;
let cutoff_date = Some("2025-01-02");
let mut items = Vec::new();
let mut pos = 0;
let limit = 10;
while items.len() < limit {
if let Some(start) = rss[pos..].find("<item>") {
let s = pos + start;
let end = rss[s..].find("</item>").map_or(rss.len(), |e| s + e + 7);
let chunk = &rss[s..end];
let title = extract_between(chunk, "<title>", "</title>").unwrap_or_default();
let link = extract_between(chunk, "<link>", "</link>").unwrap_or_default();
let raw_date = extract_between(chunk, "<pubDate>", "</pubDate>")
.map(|d| d.trim().to_string())
.unwrap_or_default();
let date = strip_time_and_tz(&raw_date);
if let Some(cutoff) = cutoff_date
&& date.as_str() < cutoff
{
break;
}
items.push(crate::state::NewsItem {
date,
title,
url: link,
});
pos = end;
} else {
break;
}
}
assert!(items.len() <= 3, "Should not exceed total items");
if !items.is_empty() {
}
}
#[test]
fn test_parse_news_html_resolves_links() {
let html = r#"<div class="article-content">
<p>Absolute link: <a href="https://example.com">Example</a></p>
<p>Relative link: <a href="/news/item">News Item</a></p>
</div>"#;
let parsed = parse_arch_news_html(html, Some("https://archlinux.org"));
assert!(parsed.contains("https://example.com"));
assert!(parsed.contains("https://archlinux.org/news/item"));
}
#[test]
fn test_parse_news_html_preserves_lists() {
let html = r#"
<div class="article-content">
2025-01-01
<ul>
<li>First item</li>
<li>Second item</li>
</ul>
</div>
"#;
let parsed = parse_arch_news_html(html, None);
assert!(
parsed.contains("•"),
"Should contain bullet character, got: {parsed:?}"
);
assert!(
parsed.contains("First item"),
"Should contain first item text, got: {parsed:?}"
);
assert!(
parsed.contains("Second item"),
"Should contain second item text, got: {parsed:?}"
);
}
#[test]
fn test_parse_news_html_preserves_pre() {
let html = r#"<div class="article-content">
<pre>
Line 1
Line 2
Line 3
</pre>
</div>"#;
let parsed = parse_arch_news_html(html, None);
assert!(parsed.contains("Line 1"));
assert!(parsed.contains("Line 2"));
assert!(parsed.contains("Line 3"));
}
#[test]
fn test_parse_news_html_formats_code() {
let html = r#"<div class="article-content">
<p>Run <code>pacman -Syu</code> to update.</p>
</div>"#;
let parsed = parse_arch_news_html(html, None);
assert!(parsed.contains("`pacman -Syu`"));
}
#[test]
fn test_parse_news_html_extracts_package_metadata() {
let html = r#"<!DOCTYPE html>
<html>
<body>
<div class="article-content">
<h1>Package: xterm</h1>
<table>
<tr><th>Upstream URL</th><td><a href="https://example.com">https://example.com</a></td></tr>
<tr><th>License(s)</th><td>MIT</td></tr>
</table>
</div>
</body>
</html>"#;
let parsed = parse_arch_news_html(html, Some("https://archlinux.org/packages/x86_64/xterm"));
assert!(parsed.contains("Package Info:"));
assert!(parsed.contains("Upstream URL: https://example.com"));
assert!(parsed.contains("License(s): MIT"));
}