use rustbot::bot::module::{Action, Command, Module}; use rustbot::bot::modules::crypto::format_coins; use rustbot::bot::modules::dict::parse_definition; use rustbot::bot::modules::fun::Fun; use rustbot::bot::modules::funweb::{ format_advice, format_catfact, format_fact, format_joke, format_worldtime, }; use rustbot::bot::modules::weather::format_forecast; fn cmd<'a>(name: &'a str, args: &'a [&'a str]) -> Command<'a> { Command { sender: "u", reply_to: "#c", name, args, prefix: ">", network: "t", } } // ---- funweb formatters ---- #[test] fn joke_single_and_twopart() { assert_eq!( format_joke(r#"{"type":"single","joke":"UDP joke."}"#), "UDP joke." ); assert_eq!( format_joke(r#"{"type":"twopart","setup":"knock knock","delivery":"who?"}"#), "knock knock … who?" ); } #[test] fn fact_catfact_advice() { assert_eq!( format_fact(r#"{"text":"the moon is dry"}"#), "the moon is dry" ); assert_eq!(format_catfact(r#"{"fact":"cats purr"}"#), "🐱 cats purr"); assert_eq!( format_advice(r#"{"slip":{"id":1,"advice":"drink water"}}"#), "💡 drink water" ); } #[test] fn worldtime_formats_time_and_day() { let out = format_worldtime( "Europe/Paris", r#"{"time":"04:21","dayOfWeek":"Thursday","timeZone":"Europe/Paris"}"#, ); assert!(out.contains("Europe/Paris"), "{out}"); assert!(out.contains("04:21") && out.contains("Thursday"), "{out}"); } // ---- crypto: coins ---- #[test] fn coins_lists_symbols_and_prices() { let out = format_coins( r#"[{"symbol":"btc","current_price":64183},{"symbol":"eth","current_price":3200},{"symbol":"xrp","current_price":0.5123}]"#, ); assert!(out.starts_with("top coins:"), "{out}"); assert!(out.contains("BTC $64183"), "{out}"); assert!(out.contains("ETH $3200"), "{out}"); assert!(out.contains("XRP $0.5123"), "{out}"); // sub-$1 keeps 4 decimals } // ---- weather: forecast ---- #[test] fn forecast_summarizes_days() { let body = r#"{"weather":[{"date":"2026-07-30","maxtempC":"34","mintempC":"22","hourly":[{},{},{},{},{"weatherDesc":[{"value":"Sunny"}]}]}]}"#; let out = format_forecast(body, "Paris"); assert!(out.starts_with("Paris:"), "{out}"); assert!(out.contains("07-30"), "{out}"); assert!(out.contains("Sunny") && out.contains("34/22"), "{out}"); } // ---- dict: RFC 2229 parsing ---- #[test] fn dict_parses_first_definition() { let resp = "220 banner\r\n150 1 definitions retrieved\r\n151 \"cat\" wn \"WordNet\"\r\ncat\r\n n 1: feline mammal [syn: {cat}, {true cat}]\r\n.\r\n250 ok\r\n"; let def = parse_definition(resp).expect("a definition"); assert!(def.contains("feline mammal"), "{def}"); assert!(!def.contains('{'), "braces should be stripped: {def}"); } #[test] fn dict_no_match_and_no_header_are_none() { assert!(parse_definition("220 banner\r\n552 no match\r\n250 ok\r\n").is_none()); assert!(parse_definition("220 banner\r\n250 ok\r\n").is_none()); } // ---- fun (local) smoke tests ---- fn one_reply(actions: Vec) -> String { match actions.as_slice() { [Action::Reply(s)] => s.clone(), _ => panic!("expected one reply"), } } #[test] fn fun_basics() { let mut f = Fun::new(); assert!(one_reply(f.on_command(&cmd("8ball", &["will it work?"]))).contains("🎱")); assert!(one_reply(f.on_command(&cmd("drinks", &[]))).contains("coffee")); assert!(one_reply(f.on_command(&cmd("catgif", &[]))).contains("cataas")); let rps = one_reply(f.on_command(&cmd("rock", &[]))); assert!(rps.contains("you:") && rps.contains("me:"), "{rps}"); } #[test] fn coffee_is_a_ctcp_action() { let mut f = Fun::new(); match f.on_command(&cmd("coffee", &["alice"])).as_slice() { [Action::Raw(line)] => { assert!(line.starts_with("PRIVMSG #c :\u{1}ACTION "), "{line}"); assert!(line.contains("alice") && line.contains("coffee"), "{line}"); } _ => panic!("expected a raw ACTION"), } }