rubot/tests/karma_web.rs
2026-07-30 00:48:57 +00:00

142 lines
5.3 KiB
Rust

use rustbot::i18n::Locale;
use rustbot::karma_web::{parse_board, parse_events, parse_meta, render};
fn board_with_events(net: &str, store: &str, log: &str) -> rustbot::karma_web::Board {
let mut b = parse_board(net, store);
b.events = parse_events(log);
b
}
#[test]
fn parses_flat_number_format() {
let b = parse_board("libera", r#"{"romaka":2}"#);
assert_eq!(b.rows.len(), 1);
assert_eq!(b.rows[0].thing, "romaka");
assert_eq!(b.rows[0].count, 2);
}
#[test]
fn parses_object_format_with_reason_and_by() {
let b = parse_board("t", r#"{"rust":{"n":5,"why":"clean PR","by":"alice"}}"#);
assert_eq!(b.rows[0].count, 5);
assert_eq!(b.rows[0].reason.as_deref(), Some("clean PR"));
assert_eq!(b.rows[0].by.as_deref(), Some("alice"));
}
#[test]
fn sorts_descending_by_count() {
let b = parse_board("t", r#"{"a":1,"b":9,"c":-3}"#);
let order: Vec<&str> = b.rows.iter().map(|r| r.thing.as_str()).collect();
assert_eq!(order, vec!["b", "a", "c"]);
}
#[test]
fn parses_event_log_lines() {
let log = "{\"t\":100,\"o\":\"rust\",\"d\":1,\"by\":\"alice\",\"why\":\"clean PR\"}\n\
{\"t\":200,\"o\":\"rust\",\"d\":-1,\"by\":\"bob\"}\n\
\n\
garbage line\n";
let ev = parse_events(log);
assert_eq!(ev.len(), 2);
assert_eq!(ev[0].thing, "rust");
assert_eq!(ev[0].delta, 1);
assert_eq!(ev[0].by.as_deref(), Some("alice"));
assert_eq!(ev[0].why.as_deref(), Some("clean PR"));
assert_eq!(ev[1].delta, -1);
assert!(ev[1].why.is_none());
}
#[test]
fn renders_hero_podium_and_scores() {
let b = parse_board(
"libera",
r#"{"rust":{"n":5,"why":"clean PR","by":"alice"}}"#,
);
let html = render(&[b], 0, Locale::En);
assert!(html.contains(">karma<"), "wordmark missing");
assert!(html.contains("podium"), "no podium");
assert!(html.contains(">rust<"), "champion nick missing");
assert!(html.contains(">+5<"), "score missing");
assert!(html.contains("clean PR"), "reason missing");
}
#[test]
fn renders_activity_and_givers_from_events() {
let log = "{\"t\":100,\"o\":\"rust\",\"d\":1,\"by\":\"alice\",\"why\":\"clean PR\"}\n\
{\"t\":200,\"o\":\"docs\",\"d\":1,\"by\":\"alice\"}\n";
let b = board_with_events("libera", r#"{"rust":1,"docs":1}"#, log);
let html = render(&[b], 0, Locale::En);
assert!(html.contains("recent activity"), "no activity section");
assert!(html.contains("top givers"), "no givers section");
assert!(html.contains("gave"), "no feed verb");
assert!(html.contains("2 gifts"), "giver count wrong");
assert!(html.contains("data-t=\"200\""), "feed newest-first missing");
}
#[test]
fn empty_states_are_friendly() {
let html = render(&[], 0, Locale::En);
assert!(html.contains("No karma yet"), "{html}");
let b = parse_board("libera", r#"{"romaka":2}"#);
let html = render(&[b], 0, Locale::En);
assert!(html.contains("Nothing yet"), "empty activity state missing");
assert!(html.contains("Be the first"), "empty givers state missing");
}
#[test]
fn renders_spanish_chrome() {
let log = "{\"t\":100,\"o\":\"rust\",\"d\":1,\"by\":\"nadia\",\"why\":\"buen PR\"}\n";
let b = board_with_events("libera", r#"{"rust":1}"#, log);
let html = render(&[b], 0, Locale::Es);
assert!(html.contains("karma neto"), "es stat label missing");
assert!(
html.contains("actividad reciente"),
"es activity heading missing"
);
assert!(html.contains("top donantes"), "es givers heading missing");
assert!(html.contains("le dio a"), "es feed verb missing");
// relative-time config for the client is Spanish
assert!(html.contains("\"pre\":\"hace \""), "es time config missing");
// user content is never translated
assert!(html.contains("buen PR") && html.contains(">rust<"));
}
#[test]
fn parses_meta_server_and_channels() {
let (server, channels) =
parse_meta(r##"{"server":"irc.libera.chat","channels":["#argentina","#ar"]}"##);
assert_eq!(server, "irc.libera.chat");
assert_eq!(channels, vec!["#argentina".to_string(), "#ar".to_string()]);
// missing/garbage meta degrades to empty
assert_eq!(parse_meta("not json"), (String::new(), Vec::new()));
}
#[test]
fn renders_source_channel_and_server() {
let mut b = parse_board("libera", r#"{"romaka":2}"#);
b.server = "irc.libera.chat".to_string();
b.channels = vec!["#argentina".to_string()];
let en = render(&[b], 0, Locale::En);
assert!(en.contains("#argentina"), "channel missing: {en}");
assert!(en.contains("irc.libera.chat"), "server missing");
assert!(en.contains(">from<"), "from label missing");
// localized label
let mut b2 = parse_board("libera", r#"{"romaka":2}"#);
b2.channels = vec!["#argentina".to_string()];
let es = render(&[b2], 0, Locale::Es);
assert!(es.contains(">desde<") && es.contains("#argentina"), "{es}");
}
#[test]
fn escapes_untrusted_irc_input_everywhere() {
let log = "{\"t\":1,\"o\":\"<script>\",\"d\":1,\"by\":\"a&b\",\"why\":\"\\\"><img src=x>\"}\n";
let b = board_with_events(
"t",
r#"{"<script>":{"n":1,"why":"\"><img src=x>","by":"a&b"}}"#,
log,
);
let html = render(&[b], 0, Locale::En);
assert!(!html.contains("<img"), "raw img tag leaked");
assert!(html.contains("&lt;script&gt;"), "nick not escaped");
assert!(html.contains("a&amp;b"), "giver not escaped");
}