Packages
rbt_weave
0.2.64
0.2.67
0.2.66
0.2.65
0.2.64
0.2.63
0.2.62
0.2.61
0.2.60
0.2.59
0.2.58
0.2.57
0.2.56
0.2.55
0.2.54
0.2.53
0.2.52
0.2.51
0.2.50
0.2.49
0.2.48
0.2.47
0.2.46
0.2.45
0.2.44
0.2.43
0.2.42
0.2.41
0.2.40
0.2.39
0.2.38
0.2.37
0.2.36
0.2.35
0.2.34
0.2.33
0.2.32
0.2.31
0.2.30
0.2.29
0.2.28
0.2.27
0.2.26
0.2.25
0.2.24
0.2.23
0.2.22
0.2.21
0.2.20
0.2.19
0.2.18
0.2.17
0.2.16
0.2.15
0.2.14
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.0
Graph conflict detection and image processing NIFs for OSINT knowledge graphs, powered by Rust.
Current section
Files
Jump to
Current section
Files
crates/weave-content/src/html/tests.rs
use super::*;
use crate::domain::Jurisdiction;
use crate::output::{CaseOutput, NodeOutput, RelOutput};
use crate::parser::SourceEntry;
use escape::{escape, format_enum, render_inline_markdown, truncate};
use thumbnail::{THUMB_KEY_HEX_LEN, rewrite_thumbnail_url, thumbnail_key};
#[allow(clippy::too_many_lines)]
fn make_case() -> CaseOutput {
CaseOutput {
id: "01TESTCASE0000000000000000".into(),
case_id: "test-case".into(),
title: "Test Corruption Case".into(),
summary: "A politician was caught accepting bribes.".into(),
tags: vec!["bribery".into(), "government".into()],
slug: None,
case_type: None,
amounts: vec![],
status: None,
tagline: None,
nodes: vec![
NodeOutput {
id: "01AAA".into(),
label: "person".into(),
name: "John Doe".into(),
slug: Some("people/id/john-doe--governor-of-test-province".into()),
qualifier: Some("Governor of Test Province".into()),
description: None,
thumbnail: Some("https://files.example.com/thumb.webp".into()),
aliases: vec![],
urls: vec![],
role: vec!["politician".into()],
nationality: vec!["ID".into()],
date_of_birth: None,
date_of_death: None,
place_of_birth: None,
status: Some("convicted".into()),
org_type: None,
jurisdiction: None,
headquarters: None,
founded_date: None,
registration_number: None,
event_type: None,
occurred_at: None,
severity: None,
doc_type: None,
issued_at: None,
issuing_authority: None,
case_number: None,
case_type: None,
tagline: None,
amounts: vec![],
asset_type: None,
value: None,
tags: vec![],
},
NodeOutput {
id: "01BBB".into(),
label: "organization".into(),
name: "KPK".into(),
slug: Some("organizations/id/kpk--anti-corruption-commission".into()),
qualifier: Some("Anti-Corruption Commission".into()),
description: None,
thumbnail: None,
aliases: vec![],
urls: vec![],
role: vec![],
nationality: Vec::new(),
date_of_birth: None,
date_of_death: None,
place_of_birth: None,
status: None,
org_type: Some("government_agency".into()),
jurisdiction: Some(Jurisdiction {
country: "ID".into(),
subdivision: None,
}),
headquarters: None,
founded_date: None,
registration_number: None,
event_type: None,
occurred_at: None,
severity: None,
doc_type: None,
issued_at: None,
issuing_authority: None,
case_number: None,
case_type: None,
tagline: None,
amounts: vec![],
asset_type: None,
value: None,
tags: vec![],
},
NodeOutput {
id: "01CCC".into(),
label: "event".into(),
name: "Arrest".into(),
slug: None,
qualifier: None,
description: Some("John Doe arrested by KPK.".into()),
thumbnail: None,
aliases: vec![],
urls: vec![],
role: vec![],
nationality: Vec::new(),
date_of_birth: None,
date_of_death: None,
place_of_birth: None,
status: None,
org_type: None,
jurisdiction: None,
headquarters: None,
founded_date: None,
registration_number: None,
event_type: Some("arrest".into()),
occurred_at: Some("2024-03-15".into()),
severity: None,
doc_type: None,
issued_at: None,
issuing_authority: None,
case_number: None,
case_type: None,
tagline: None,
amounts: vec![],
asset_type: None,
value: None,
tags: vec![],
},
],
relationships: vec![RelOutput {
id: "01DDD".into(),
rel_type: "investigated_by".into(),
source_id: "01BBB".into(),
source_label: "person".into(),
target_id: "01CCC".into(),
target_label: "organization".into(),
source_urls: vec![],
description: None,
amounts: vec![],
valid_from: None,
valid_until: None,
}],
sources: vec![SourceEntry::Url("https://example.com/article".into())],
}
}
#[test]
fn render_case_produces_valid_html() {
let case = make_case();
let config = HtmlConfig::default();
let html = render_case(&case, &config).unwrap();
assert!(html.starts_with("<article"));
assert!(html.ends_with("</article>\n"));
assert!(html.contains("data-og-title=\"Test Corruption Case\""));
assert!(html.contains("data-og-description="));
assert!(html.contains("<h1 itemprop=\"headline\">Test Corruption Case</h1>"));
assert!(html.contains("loom-tag"));
assert!(html.contains("bribery"));
assert!(html.contains("John Doe"));
assert!(html.contains("KPK"));
assert!(html.contains("Arrest"));
assert!(html.contains("2024-03-15"));
assert!(html.contains("application/ld+json"));
// Case header has wall link
assert!(html.contains("View on the wall"));
assert!(html.contains("/walls/01TESTCASE0000000000000000"));
}
#[test]
fn render_case_with_country_slug() {
let mut case = make_case();
case.slug = Some("cases/id/corruption/2024/test-case".into());
let config = HtmlConfig::default();
let html = render_case(&case, &config).unwrap();
// Country badge in header
assert!(html.contains("loom-country-badge"));
assert!(html.contains("Indonesia"));
// OG title includes country
assert!(html.contains("data-og-title=\"Test Corruption Case — Indonesia\""));
}
#[test]
fn render_case_with_multinational_slug() {
let mut case = make_case();
case.slug = Some("cases/multinational/money-laundering/2023/test-case".into());
let config = HtmlConfig::default();
let html = render_case(&case, &config).unwrap();
assert!(html.contains("loom-country-badge"));
assert!(html.contains("Multinational"));
assert!(html.contains("data-og-title=\"Test Corruption Case — Multinational\""));
}
#[test]
fn render_case_without_slug_has_no_country() {
let case = make_case();
let config = HtmlConfig::default();
let html = render_case(&case, &config).unwrap();
assert!(!html.contains("loom-country-badge"));
}
#[test]
fn render_cases_list_omits_country_badge() {
let case = make_case();
let config = HtmlConfig::default();
let person = &case.nodes[0];
let cases_list = vec![(
"cases/id/corruption/2024/test-case".into(),
"Test Corruption Case".into(),
)];
let html = render_person(person, &cases_list, &config).unwrap();
// Case list should NOT include country badges (redundant)
assert!(!html.contains("loom-country-badge"));
assert!(html.contains("Test Corruption Case"));
assert!(html.contains("loom-case-list"));
}
#[test]
fn render_case_summary_renders_inline_markdown() {
let mut case = make_case();
case.summary = "**Amount not publicly documented.**".into();
let config = HtmlConfig::default();
let html = render_case(&case, &config).unwrap();
assert!(html.contains(
"<p class=\"loom-summary\" itemprop=\"description\"><strong>Amount not publicly documented.</strong></p>"
));
}
#[test]
fn render_case_has_sources() {
let case = make_case();
let config = HtmlConfig::default();
let html = render_case(&case, &config).unwrap();
assert!(html.contains("Sources"));
assert!(html.contains("https://example.com/article"));
}
#[test]
fn render_case_entity_cards_link_to_static_views() {
let case = make_case();
let config = HtmlConfig::default();
let html = render_case(&case, &config).unwrap();
// Entity cards should link to static views, not wall
assert!(html.contains("href=\"/people/id/john-doe--governor-of-test-province\""));
assert!(html.contains("href=\"/organizations/id/kpk--anti-corruption-commission\""));
// Should NOT link to /walls/ for entities with slugs
assert!(!html.contains("href=\"/walls/01AAA\""));
assert!(!html.contains("href=\"/walls/01BBB\""));
}
#[test]
fn render_case_entity_cards_fallback_to_walls() {
let mut case = make_case();
let config = HtmlConfig::default();
// Remove slugs from entities
for node in &mut case.nodes {
node.slug = None;
}
let html = render_case(&case, &config).unwrap();
// Without slugs, entity cards fall back to wall links
assert!(html.contains("href=\"/walls/01AAA\""));
assert!(html.contains("href=\"/walls/01BBB\""));
}
#[test]
fn render_case_omits_connections_table() {
let case = make_case();
let config = HtmlConfig::default();
let html = render_case(&case, &config).unwrap();
// Connections table is intentionally omitted — relationships are
// already expressed in People/Organizations cards and Timeline
assert!(!html.contains("Connections"));
assert!(!html.contains("loom-rel-table"));
}
#[test]
fn render_person_page() {
let case = make_case();
let config = HtmlConfig::default();
let person = &case.nodes[0];
let cases_list = vec![("test-case".into(), "Test Corruption Case".into())];
let html = render_person(person, &cases_list, &config).unwrap();
assert!(html.contains("itemtype=\"https://schema.org/Person\""));
assert!(html.contains("John Doe"));
assert!(html.contains("Governor of Test Province"));
assert!(html.contains("/walls/01AAA"));
assert!(html.contains("Test Corruption Case"));
assert!(html.contains("application/ld+json"));
// Nationality links to country page
assert!(html.contains("loom-country-link"));
assert!(html.contains("<a href=\"/countries/id\""));
}
#[test]
fn render_organization_page() {
let case = make_case();
let config = HtmlConfig::default();
let org = &case.nodes[1];
let cases_list = vec![("test-case".into(), "Test Corruption Case".into())];
let html = render_organization(org, &cases_list, &config).unwrap();
assert!(html.contains("itemtype=\"https://schema.org/Organization\""));
assert!(html.contains("KPK"));
// Jurisdiction links to country page
assert!(html.contains("loom-country-link"));
assert!(html.contains("<a href=\"/countries/id\""));
}
#[test]
fn render_sitemap_includes_all_urls() {
let cases = vec![("cases/id/corruption/2024/test-case".into(), "Case 1".into())];
let people = vec![("people/id/john-doe".into(), "John".into())];
let orgs = vec![("organizations/id/test-corp".into(), "Corp".into())];
let xml = render_sitemap(
&cases,
&people,
&orgs,
&["id".into()],
&["bribery".into()],
"https://redberrythread.org",
);
assert!(xml.contains("<?xml"));
assert!(xml.contains("/cases/id/corruption/2024/test-case"));
assert!(xml.contains("/people/id/john-doe"));
assert!(xml.contains("/organizations/id/test-corp"));
assert!(xml.contains("/countries/id"));
assert!(xml.contains("/tags/bribery"));
assert!(xml.contains("<url><loc>https://redberrythread.org/countries</loc></url>"));
assert!(xml.contains("<url><loc>https://redberrythread.org/tags</loc></url>"));
}
#[test]
fn escape_html_special_chars() {
assert_eq!(escape("<script>"), "<script>");
assert_eq!(escape("AT&T"), "AT&T");
assert_eq!(escape("\"quoted\""), ""quoted"");
}
#[test]
fn render_inline_markdown_bold_and_italic() {
assert_eq!(
render_inline_markdown("**Amount not publicly documented.**"),
"<strong>Amount not publicly documented.</strong>"
);
assert_eq!(
render_inline_markdown("Some *italic* text"),
"Some <em>italic</em> text"
);
}
#[test]
fn render_inline_markdown_escapes_plain_text() {
// pulldown-cmark decodes HTML entities, then we re-escape
assert_eq!(render_inline_markdown("A & B"), "A & B");
assert_eq!(
render_inline_markdown("use `x < y`"),
"use <code>x < y</code>"
);
}
#[test]
fn render_inline_markdown_links() {
assert_eq!(
render_inline_markdown("[KPK](https://kpk.go.id) arrested him"),
"<a href=\"https://kpk.go.id\" rel=\"noopener noreferrer\">KPK</a> arrested him"
);
}
#[test]
fn truncate_short_string() {
assert_eq!(truncate("hello", 10), "hello");
}
#[test]
fn truncate_long_string() {
let long = "a".repeat(200);
let result = truncate(&long, 120);
assert!(result.len() <= 120);
assert!(result.ends_with("..."));
}
#[test]
fn format_enum_underscore() {
assert_eq!(format_enum("investigated_by"), "Investigated By");
assert_eq!(format_enum("custom:Special Type"), "Special Type");
}
#[test]
fn thumbnail_key_deterministic() {
let k1 = thumbnail_key("https://example.com/photo.jpg");
let k2 = thumbnail_key("https://example.com/photo.jpg");
assert_eq!(k1, k2);
assert!(k1.starts_with("thumbnails/"));
assert!(
std::path::Path::new(&k1)
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("webp")),
"key should end with .webp: {k1}"
);
// Key hex part is 32 chars
let hex_part = k1
.strip_prefix("thumbnails/")
.and_then(|s| s.strip_suffix(".webp"))
.unwrap_or("");
assert_eq!(hex_part.len(), THUMB_KEY_HEX_LEN);
}
#[test]
fn thumbnail_key_different_urls_differ() {
let k1 = thumbnail_key("https://example.com/a.jpg");
let k2 = thumbnail_key("https://example.com/b.jpg");
assert_ne!(k1, k2);
}
#[test]
fn rewrite_thumbnail_url_no_config() {
let config = HtmlConfig::default();
let result = rewrite_thumbnail_url("https://example.com/photo.jpg", &config);
assert_eq!(result, "https://example.com/photo.jpg");
}
#[test]
fn rewrite_thumbnail_url_with_base() {
let config = HtmlConfig {
thumbnail_base_url: Some("http://files.garage.local:3902/files".into()),
};
let result = rewrite_thumbnail_url("https://example.com/photo.jpg", &config);
assert!(result.starts_with("http://files.garage.local:3902/files/thumbnails/"));
assert!(
std::path::Path::new(&result)
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("webp")),
"result should end with .webp: {result}"
);
assert!(!result.contains("example.com"));
}
#[test]
fn rewrite_thumbnail_url_already_rewritten() {
let config = HtmlConfig {
thumbnail_base_url: Some("https://files.redberrythread.org".into()),
};
let already =
"https://files.redberrythread.org/thumbnails/6fc3a49567393053be6138aa346fa97a.webp";
let result = rewrite_thumbnail_url(already, &config);
assert_eq!(
result, already,
"should not double-hash already-rewritten URLs"
);
}
#[test]
fn render_case_rewrites_thumbnails() {
let case = make_case();
let config = HtmlConfig {
thumbnail_base_url: Some("http://garage.local/files".into()),
};
let html = render_case(&case, &config).unwrap();
// Original URL should not appear in img src
assert!(!html.contains("src=\"https://files.example.com/thumb.webp\""));
// Rewritten URL should appear
assert!(html.contains("src=\"http://garage.local/files/thumbnails/"));
// OG image should also be rewritten
assert!(html.contains("data-og-image=\"http://garage.local/files/thumbnails/"));
}
#[test]
fn render_person_rewrites_thumbnails() {
let case = make_case();
let person = &case.nodes[0];
let config = HtmlConfig {
thumbnail_base_url: Some("http://garage.local/files".into()),
};
let html = render_person(person, &[], &config).unwrap();
assert!(!html.contains("src=\"https://files.example.com/thumb.webp\""));
assert!(html.contains("src=\"http://garage.local/files/thumbnails/"));
}
#[test]
fn render_case_with_related_cases() {
let mut case = make_case();
// Add a related_to relationship and target case node
case.relationships.push(RelOutput {
id: "01RELID".into(),
rel_type: "related_to".into(),
source_id: "01TESTCASE0000000000000000".into(),
source_label: "case".into(),
target_id: "01TARGETCASE000000000000000".into(),
target_label: "case".into(),
source_urls: vec![],
description: Some("Connected bribery scandal".into()),
amounts: vec![],
valid_from: None,
valid_until: None,
});
case.nodes.push(NodeOutput {
id: "01TARGETCASE000000000000000".into(),
label: "case".into(),
name: "Target Scandal Case".into(),
slug: Some("cases/id/corruption/2002/target-scandal".into()),
qualifier: None,
description: None,
thumbnail: None,
aliases: vec![],
urls: vec![],
role: vec![],
nationality: Vec::new(),
date_of_birth: None,
date_of_death: None,
place_of_birth: None,
status: None,
org_type: None,
jurisdiction: None,
headquarters: None,
founded_date: None,
registration_number: None,
event_type: None,
occurred_at: None,
severity: None,
doc_type: None,
issued_at: None,
issuing_authority: None,
case_number: None,
case_type: None,
tagline: None,
amounts: vec![],
asset_type: None,
value: None,
tags: vec![],
});
let config = HtmlConfig::default();
let html = render_case(&case, &config).unwrap();
assert!(html.contains("loom-related-cases"));
assert!(html.contains("Related Cases"));
assert!(html.contains("Target Scandal Case"));
assert!(html.contains("loom-related-card"));
assert!(html.contains("Connected bribery scandal"));
}
#[test]
fn render_tag_page_shows_country() {
let cases = vec![TagCaseEntry {
slug: "cases/id/corruption/2024/test-case".into(),
title: "Test Corruption Case".into(),
amounts: vec![],
}];
let html = render_tag_page("bribery", &cases).unwrap();
assert!(html.contains("loom-country-badge"));
assert!(html.contains("Indonesia"));
}
#[test]
fn render_case_without_related_cases() {
let case = make_case();
let config = HtmlConfig::default();
let html = render_case(&case, &config).unwrap();
assert!(!html.contains("loom-related-cases"));
}
#[test]
fn render_country_page_lists_cases() {
let cases = vec![
TagCaseEntry {
slug: "cases/id/corruption/2024/test-case".into(),
title: "Test Case".into(),
amounts: vec![],
},
TagCaseEntry {
slug: "cases/id/corruption/2024/another-case".into(),
title: "Another Case".into(),
amounts: vec![],
},
];
let html = render_country_page("id", &cases).unwrap();
assert!(html.contains("loom-country-page"));
assert!(html.contains("Indonesia"));
assert!(html.contains("2 cases"));
assert!(html.contains("Test Case"));
assert!(html.contains("Another Case"));
assert!(html.contains("/countries/id"));
assert!(!html.contains("loom-country-badge"));
}
#[test]
fn country_badge_is_link() {
let cases = vec![TagCaseEntry {
slug: "cases/id/corruption/2024/test-case".into(),
title: "Test".into(),
amounts: vec![],
}];
let html = render_tag_page("bribery", &cases).unwrap();
assert!(html.contains("<a href=\"/countries/id\" class=\"loom-country-badge\">"));
}
#[test]
fn render_countries_index_lists_countries() {
let countries = vec![
CountryIndexEntry {
code: "id".into(),
name: "Indonesia".into(),
case_count: 5,
},
CountryIndexEntry {
code: "my".into(),
name: "Malaysia".into(),
case_count: 3,
},
];
let html = render_countries_index(&countries).unwrap();
assert!(html.contains("loom-countries-index"));
assert!(html.contains("Countries"));
assert!(html.contains("8 cases across 2 countries"));
assert!(html.contains("<a href=\"/countries/id\">Indonesia</a>"));
assert!(html.contains("<a href=\"/countries/my\">Malaysia</a>"));
assert!(html.contains("loom-count-badge"));
assert!(html.contains("data-og-url=\"/countries\""));
}
#[test]
fn render_tags_index_lists_tags() {
let tags = vec![
TagIndexEntry {
tag: "bribery".into(),
case_count: 10,
},
TagIndexEntry {
tag: "money-laundering".into(),
case_count: 7,
},
];
let html = render_tags_index(&tags).unwrap();
assert!(html.contains("loom-tags-index"));
assert!(html.contains("Tags"));
assert!(html.contains("17 cases across 2 tags"));
assert!(html.contains("<a href=\"/tags/bribery\">bribery</a>"));
assert!(html.contains("<a href=\"/tags/money-laundering\">money laundering</a>"));
assert!(html.contains("loom-count-badge"));
assert!(html.contains("data-og-url=\"/tags\""));
}