use std::time::Duration; use anyhow::{bail, Result}; use log::*; use mobot::*; /// `TestApp` represents the state of our test bot. It contains a counter that is incremented /// every time a message is received. #[derive(Debug, Clone, Default, BotState)] struct TestApp { counter: i32, } /// This is our chat handler. This bot increments the internal counter and replies with a /// message containing the counter. async fn handle_chat_event(e: Event, state: State) -> Result { let mut state = state.get().write().await; match e.update { Update::Message(message) => { state.counter += 1; info!( "chatid:{}: pong({}): {}", message.chat.id, state.counter, message.text.clone().unwrap_or_default() ); Ok(Action::ReplyText(format!( "pong({}): {}", state.counter, message.text.unwrap_or_default() ))) } Update::EditedMessage(message) => { info!( "chatid:{}: edited_message: {}", message.chat.id, message.text.clone().unwrap_or_default() ); Ok(Action::ReplyText(format!( "edited_pong: {}", message.text.unwrap_or_default() ))) } _ => bail!("Unhandled update"), } } #[tokio::test] async fn it_works() { mobot::init_logger(); // Create a FakeAPI and attach it to the client. Any Telegram requests are now forwarded // to `fakeserver` instead. let fakeserver = fake::FakeAPI::new(); let client = Client::new("token".to_string()).with_post_handler(fakeserver.clone()); // Keep the Telegram poll timeout short for testing. The default Telegram poll timeout is 60s. let mut router = Router::new(client).with_poll_timeout_s(1); // Since we're passing ownership of the Router to a background task, grab the // shutdown channels so we can shut it down from this task. let (shutdown_notifier, shutdown_tx) = router.shutdown(); // Our bot is a ping bot. Add the handler to the router (see bin/ping.rs). router.add_route(Route::Default, handle_chat_event); // Start the router in a background task. tokio::spawn(async move { info!("Starting router..."); router.start().await; }); // We're in the foreground. Create a new chat session with the bot, providing your // username. This shows up in the `from` field of messages. let chat = fakeserver.create_chat("qubyte").await; // Send the message "ping1", expect the response "pong(1): ping1" chat.send_text("ping1").await.unwrap(); assert_eq!( chat.recv_update().await.unwrap().to_string(), "pong(1): ping1" ); // Send the message "ping2", expect the response "pong(2): ping2" chat.send_text("ping2").await.unwrap(); assert_eq!( chat.recv_update().await.unwrap().to_string(), "pong(2): ping2" ); // Optional: validate there's no more messages from the bot, by waiting two seconds // for more messages. assert!( tokio::time::timeout(Duration::from_millis(2000), chat.recv_update()) .await .is_err() ); // All done shutdown the router, and wait for it to complete. info!("Shutting down..."); shutdown_tx.send(()).await.unwrap(); shutdown_notifier.notified().await; } #[tokio::test] async fn multiple_chats() { mobot::init_logger(); let fakeserver = fake::FakeAPI::new(); let client = Client::new("token".to_string()).with_post_handler(fakeserver.clone()); // Keep the timeout short for testing. let mut router = Router::new(client).with_poll_timeout_s(1); let (shutdown_notifier, shutdown_tx) = router.shutdown(); // We add a helper handler that logs all incoming messages. router.add_route(Route::Default, handle_chat_event); tokio::spawn(async move { info!("Starting router..."); router.start().await; }); let chat1 = fakeserver.create_chat("qubyte").await; let chat2 = fakeserver.create_chat("qubyte").await; chat1.send_text("ping1").await.unwrap(); assert_eq!( chat1.recv_update().await.unwrap().to_string(), "pong(1): ping1" ); chat1.send_text("ping2").await.unwrap(); assert_eq!( chat1.recv_update().await.unwrap().to_string(), "pong(2): ping2" ); chat2.send_text("ping1").await.unwrap(); assert_eq!( chat2.recv_update().await.unwrap().to_string(), "pong(1): ping1" ); info!("Shutting down..."); shutdown_tx.send(()).await.unwrap(); shutdown_notifier.notified().await; } #[tokio::test] async fn multiple_chats_new_state() { mobot::init_logger(); let fakeserver = fake::FakeAPI::new(); let client = Client::new("token".to_string()).with_post_handler(fakeserver.clone()); // Keep the timeout short for testing. let mut router = Router::new(client) .with_poll_timeout_s(1) .with_state(TestApp { counter: 1000 }); let (shutdown_notifier, shutdown_tx) = router.shutdown(); // We add a helper handler that logs all incoming messages. router.add_route(Route::Default, handle_chat_event); tokio::spawn(async move { info!("Starting router..."); router.start().await; }); let chat1 = fakeserver.create_chat("qubyte").await; let chat2 = fakeserver.create_chat("qubyte").await; chat1.send_text("ping1").await.unwrap(); assert_eq!( chat1.recv_update().await.unwrap().to_string(), "pong(1001): ping1" ); chat1.send_text("ping2").await.unwrap(); assert_eq!( chat1.recv_update().await.unwrap().to_string(), "pong(1002): ping2" ); chat2.send_text("ping1").await.unwrap(); assert_eq!( chat2.recv_update().await.unwrap().to_string(), "pong(1001): ping1" ); info!("Shutting down..."); shutdown_tx.send(()).await.unwrap(); shutdown_notifier.notified().await; } #[tokio::test] async fn add_route() { mobot::init_logger(); let fakeserver = fake::FakeAPI::new(); let client = Client::new("token".to_string()).with_post_handler(fakeserver.clone()); // Keep the timeout short for testing. let mut router = Router::new(client).with_poll_timeout_s(1); let (shutdown_notifier, shutdown_tx) = router.shutdown(); // We add a helper handler that logs all incoming messages. router .add_route( Route::Message(Matcher::Prefix("/foo".into())), handle_chat_event, ) .add_route( Route::Message(Matcher::Exact("boo".into())), handle_chat_event, ); tokio::spawn(async move { info!("Starting router..."); router.start().await; }); let chat = fakeserver.create_chat("qubyte").await; chat.send_text("ping1").await.unwrap(); // Wait two seconds for messages -- there should be none, so expect a timeout error. assert!( tokio::time::timeout(Duration::from_millis(500), chat.recv_update()) .await .is_err() ); chat.send_text("/foobar").await.unwrap(); assert_eq!( chat.recv_update().await.unwrap().to_string(), "pong(1): /foobar" ); chat.send_text("boo1").await.unwrap(); chat.send_text("boo").await.unwrap(); assert_eq!( chat.recv_update().await.unwrap().to_string(), "pong(2): boo" ); info!("Shutting down..."); shutdown_tx.send(()).await.unwrap(); shutdown_notifier.notified().await; } #[tokio::test] async fn edit_message_text() { mobot::init_logger(); let fakeserver = fake::FakeAPI::new(); let client = Client::new("token".to_string()).with_post_handler(fakeserver.clone()); // Keep the timeout short for testing. let mut router = Router::new(client).with_poll_timeout_s(1); let (shutdown_notifier, shutdown_tx) = router.shutdown(); // We add a helper handler that logs all incoming messages. router.add_route(Route::Default, handle_chat_event); tokio::spawn(async move { info!("Starting router..."); router.start().await; }); let chat1 = fakeserver.create_chat("qubyte").await; chat1.send_text("ping1").await.unwrap(); let message: api::Message = chat1.recv_update().await.unwrap().into(); assert_eq!(message.text.unwrap(), "pong(1): ping1"); chat1.edit_text(message.message_id, "ping2").await.unwrap(); assert_eq!( chat1.recv_update().await.unwrap().to_string(), "edited_pong: ping2" ); info!("Shutting down..."); shutdown_tx.send(()).await.unwrap(); shutdown_notifier.notified().await; } /// This handler displays a message with two inline keyboard buttons: "yes" and "no". async fn ask_message(e: Event, _: State<()>) -> Result { e.api .send_message( &api::SendMessageRequest::new(e.update.chat_id()?, "Push the button!") .with_parse_mode(api::ParseMode::MarkdownV2) .with_reply_markup(api::ReplyMarkup::inline_keyboard_markup(vec![vec![ api::InlineKeyboardButton::from("Yes").with_callback_data("yes"), api::InlineKeyboardButton::from("No").with_callback_data("no"), ]])), ) .await?; Ok(Action::Done) } /// This handler is called when one of the buttons above is pressed async fn ask_callback(e: Event, _: State<()>) -> Result { // Handle the callback query from the user. This happens any time a button is pressed // on the inline keyboard. let action = e.update.data()?; e.remove_inline_keyboard().await?; Ok(Action::ReplyText(format!("pressed: {}", action))) } #[tokio::test] async fn push_buttons() { mobot::init_logger(); let fakeserver = fake::FakeAPI::new(); let client = Client::new("token".to_string()).with_post_handler(fakeserver.clone()); // Keep the timeout short for testing. let mut router = Router::new(client).with_poll_timeout_s(1); let (shutdown_notifier, shutdown_tx) = router.shutdown(); // We add a helper handler that logs all incoming messages. router.add_route(Route::Message(Matcher::Any), ask_message); router.add_route(Route::CallbackQuery(Matcher::Any), ask_callback); tokio::spawn(async move { info!("Starting router..."); router.start().await; }); let chat1 = fakeserver.create_chat("qubyte").await; chat1.send_text("what?").await.unwrap(); // Expect some buttons let message: api::Message = chat1.recv_update().await.unwrap().into(); assert_eq!(message.text.unwrap(), "Push the button!"); // Push "yes" chat1.send_callback_query("yes").await.unwrap(); let event = chat1.recv_update().await.unwrap(); // Expect the reply markup to be cleared let Update::EditedMessage(_) = event else { panic!("Expected edited message (reply markup), got {:?}", event); }; // Expect the reply text to be updated with the pressed button: "yes" assert_eq!( chat1.recv_update().await.unwrap().to_string(), "pressed: yes" ); info!("Shutting down..."); shutdown_tx.send(()).await.unwrap(); shutdown_notifier.notified().await; }