karma-web: online status, user counts, join links, rising/chart/most-debated/milestones/reasons, filter+sort, expandable history, OG tags, JSON API
All checks were successful
ci / check (push) Successful in 46s
All checks were successful
ci / check (push) Successful in 46s
This commit is contained in:
parent
26496b5147
commit
6af92c1217
7 changed files with 714 additions and 101 deletions
|
|
@ -1,5 +1,5 @@
|
|||
use rustbot::i18n::Locale;
|
||||
use rustbot::karma_web::{parse_board, parse_events, parse_meta, render, Channel};
|
||||
use rustbot::karma_web::{parse_board, parse_events, parse_meta, render, render_json, Channel};
|
||||
|
||||
fn board_with_events(net: &str, store: &str, log: &str) -> rustbot::karma_web::Board {
|
||||
let mut b = parse_board(net, store);
|
||||
|
|
@ -102,51 +102,64 @@ fn renders_spanish_chrome() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn parses_meta_channels_with_and_without_topic() {
|
||||
// new format: objects with an optional topic
|
||||
let (server, channels) = parse_meta(
|
||||
r##"{"server":"irc.libera.chat","channels":[{"name":"#argentina","topic":"Bienvenidos"},{"name":"#ar"}]}"##,
|
||||
fn parses_meta_channels_topic_and_users() {
|
||||
let m = parse_meta(
|
||||
r##"{"server":"irc.libera.chat","webchat":"https://web.libera.chat/","seen":123,"channels":[{"name":"#argentina","topic":"Bienvenidos","users":42},{"name":"#ar"}]}"##,
|
||||
);
|
||||
assert_eq!(server, "irc.libera.chat");
|
||||
assert_eq!(channels.len(), 2);
|
||||
assert_eq!(channels[0].name, "#argentina");
|
||||
assert_eq!(channels[0].topic.as_deref(), Some("Bienvenidos"));
|
||||
assert!(channels[1].topic.is_none());
|
||||
// old format (bare strings) still parses
|
||||
let (_, old) = parse_meta(r##"{"server":"x","channels":["#c"]}"##);
|
||||
assert_eq!(old[0].name, "#c");
|
||||
assert!(old[0].topic.is_none());
|
||||
assert_eq!(m.server, "irc.libera.chat");
|
||||
assert_eq!(m.webchat, "https://web.libera.chat/");
|
||||
assert_eq!(m.seen, 123);
|
||||
assert_eq!(m.channels.len(), 2);
|
||||
assert_eq!(m.channels[0].name, "#argentina");
|
||||
assert_eq!(m.channels[0].topic.as_deref(), Some("Bienvenidos"));
|
||||
assert_eq!(m.channels[0].users, Some(42));
|
||||
assert!(m.channels[1].topic.is_none() && m.channels[1].users.is_none());
|
||||
// old bare-string format still parses
|
||||
let old = parse_meta(r##"{"server":"x","channels":["#c"]}"##);
|
||||
assert_eq!(old.channels[0].name, "#c");
|
||||
// garbage degrades to empty
|
||||
let (s, c) = parse_meta("not json");
|
||||
assert!(s.is_empty() && c.is_empty());
|
||||
let g = parse_meta("not json");
|
||||
assert!(g.server.is_empty() && g.channels.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renders_source_channel_server_and_topic() {
|
||||
fn renders_source_topic_users_join_and_online() {
|
||||
let mut b = parse_board("libera", r#"{"romaka":2}"#);
|
||||
b.server = "irc.libera.chat".to_string();
|
||||
b.webchat = "https://web.libera.chat/".to_string();
|
||||
b.seen = 1000;
|
||||
b.channels = vec![Channel {
|
||||
name: "#argentina".to_string(),
|
||||
topic: Some("Argentina en Libera - stats https://qadlo.example/".to_string()),
|
||||
users: Some(42),
|
||||
}];
|
||||
let en = render(&[b], 0, Locale::En);
|
||||
let en = render(&[b], 1100, Locale::En); // now close to seen -> online
|
||||
assert!(en.contains("#argentina"), "channel missing");
|
||||
assert!(en.contains("irc.libera.chat"), "server missing");
|
||||
assert!(en.contains(">from<"), "from label missing");
|
||||
// the topic text shows, and its URL becomes a safe link
|
||||
assert!(en.contains("Argentina en Libera"), "topic text missing");
|
||||
assert!(en.contains("Argentina en Libera"), "topic missing");
|
||||
assert!(
|
||||
en.contains("<a href=\"https://qadlo.example/\""),
|
||||
"topic url not linkified: {en}"
|
||||
"topic url not linkified"
|
||||
);
|
||||
// spanish label still works with no topic
|
||||
let mut b2 = parse_board("libera", r#"{"romaka":2}"#);
|
||||
b2.channels = vec![Channel {
|
||||
name: "#argentina".to_string(),
|
||||
topic: None,
|
||||
}];
|
||||
let es = render(&[b2], 0, Locale::Es);
|
||||
assert!(es.contains(">desde<") && es.contains("#argentina"), "{es}");
|
||||
assert!(en.contains("42 online"), "user count missing");
|
||||
assert!(
|
||||
en.contains("https://web.libera.chat/#argentina"),
|
||||
"join link missing"
|
||||
);
|
||||
assert!(en.contains("class=\"dot on\""), "online dot missing");
|
||||
// og description present
|
||||
assert!(
|
||||
en.contains("property=\"og:description\""),
|
||||
"og tags missing"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn online_dot_goes_offline_when_stale() {
|
||||
let mut b = parse_board("t", r#"{"x":1}"#);
|
||||
b.seen = 1000;
|
||||
let html = render(&[b], 9_999_999, Locale::En);
|
||||
assert!(html.contains("class=\"dot off\""), "should be offline");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -155,6 +168,7 @@ fn topic_cannot_inject_markup() {
|
|||
b.channels = vec![Channel {
|
||||
name: "#c".to_string(),
|
||||
topic: Some("<script>evil</script> javascript:alert(1)".to_string()),
|
||||
users: None,
|
||||
}];
|
||||
let html = render(&[b], 0, Locale::En);
|
||||
assert!(!html.contains("<script>evil"), "topic markup leaked");
|
||||
|
|
@ -162,6 +176,51 @@ fn topic_cannot_inject_markup() {
|
|||
assert!(html.contains("<script>evil"), "topic not escaped");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renders_analytics_sections() {
|
||||
let now = 2_000_000u64;
|
||||
let mut log = String::new();
|
||||
for i in 0..12u64 {
|
||||
let t = now - (12 - i) * 3600;
|
||||
let why = if i == 0 {
|
||||
",\"why\":\"clean code\""
|
||||
} else {
|
||||
""
|
||||
};
|
||||
log.push_str(&format!(
|
||||
"{{\"t\":{t},\"o\":\"rust\",\"d\":1,\"by\":\"nadia\"{why}}}\n"
|
||||
));
|
||||
}
|
||||
log.push_str(&format!(
|
||||
"{{\"t\":{},\"o\":\"php\",\"d\":-1,\"by\":\"sam\",\"why\":\"footgun\"}}\n",
|
||||
now - 1000
|
||||
));
|
||||
let b = board_with_events("libera", r#"{"rust":12,"php":-1}"#, &log);
|
||||
let html = render(&[b], now, Locale::En);
|
||||
assert!(html.contains("rising this week"), "no rising strip");
|
||||
assert!(html.contains("karma over time"), "no chart");
|
||||
assert!(html.contains("most debated"), "no most-debated");
|
||||
assert!(
|
||||
html.contains("milestones") && html.contains("reached"),
|
||||
"no milestones"
|
||||
);
|
||||
assert!(html.contains("recent reasons"), "no reasons wall");
|
||||
assert!(
|
||||
html.contains("clean code") && html.contains("footgun"),
|
||||
"reasons missing"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_json_is_machine_readable() {
|
||||
let b = parse_board("libera", r#"{"rust":{"n":5,"why":"clean","by":"a"}}"#);
|
||||
let j = render_json(&[b], 42);
|
||||
assert!(j.contains("\"generated\":42"), "{j}");
|
||||
assert!(j.contains("\"network\":\"libera\""), "{j}");
|
||||
assert!(j.contains("\"thing\":\"rust\",\"karma\":5"), "{j}");
|
||||
assert!(j.contains("\"reason\":\"clean\""), "{j}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escapes_untrusted_irc_input_everywhere() {
|
||||
let log = "{\"t\":1,\"o\":\"<script>\",\"d\":1,\"by\":\"a&b\",\"why\":\"\\\"><img src=x>\"}\n";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue