mod dispatch_support;
use dispatch_support::{client_for, client_without_retries, reporting, reports};
use rustigram_bot::dispatcher::Dispatcher;
use rustigram_bot::error::BotError;
use rustigram_bot::filter::filters;
use rustigram_bot::update_listener::polling::LongPoller;
use serde_json::{json, Value};
use std::time::Duration;
use wiremock::matchers::{method, path_regex};
use wiremock::{Mock, MockServer, Request, ResponseTemplate};
fn batch(ids: &[i64]) -> Value {
let updates: Vec<Value> = ids
.iter()
.map(|id| {
json!({
"update_id": id,
"message": {
"message_id": 1, "date": 1_700_000_000,
"chat": { "id": 42, "type": "private" },
"from": { "id": 7, "is_bot": false, "first_name": "T" },
"text": "hi"
}
})
})
.collect();
json!({ "ok": true, "result": updates })
}
async fn script(bodies: Vec<Value>) -> MockServer {
let server = MockServer::start().await;
let (last, rest) = bodies.split_last().expect("at least one body");
for body in rest {
Mock::given(method("POST"))
.and(path_regex(r".*/getUpdates$"))
.respond_with(ResponseTemplate::new(200).set_body_json(body.clone()))
.up_to_n_times(1)
.mount(&server)
.await;
}
Mock::given(method("POST"))
.and(path_regex(r".*/getUpdates$"))
.respond_with(ResponseTemplate::new(200).set_body_json(last.clone()))
.mount(&server)
.await;
server
}
async fn offsets(server: &MockServer) -> Vec<Option<i64>> {
server
.received_requests()
.await
.expect("the mock records requests")
.iter()
.map(|r: &Request| {
serde_json::from_slice::<Value>(&r.body)
.ok()
.and_then(|b| b.get("offset").and_then(Value::as_i64))
})
.collect()
}
#[tokio::test]
async fn the_offset_advances_and_is_sent_on_the_next_call() {
let server = script(vec![batch(&[10, 11, 12]), batch(&[20])]).await;
let mut poller = LongPoller::new(client_for(&server.uri()));
let first = poller.next_batch().await.expect("the first batch arrives");
assert_eq!(first.len(), 3);
let second = poller.next_batch().await.expect("the second batch arrives");
assert_eq!(second.len(), 1);
assert_eq!(
offsets(&server).await,
vec![None, Some(13)],
"the first call carries no offset and the second must carry \
last.update_id + 1"
);
}
#[tokio::test]
async fn an_empty_batch_does_not_move_the_offset() {
let server = script(vec![batch(&[10]), batch(&[]), batch(&[])]).await;
let mut poller = LongPoller::new(client_for(&server.uri()));
poller.next_batch().await.expect("a batch arrives");
poller.next_batch().await.expect("an empty batch arrives");
poller.next_batch().await.expect("another empty batch");
assert_eq!(
offsets(&server).await,
vec![None, Some(11), Some(11)],
"an empty batch must leave the offset untouched"
);
}
#[tokio::test]
async fn the_offset_follows_the_last_update_in_the_batch() {
let server = script(vec![batch(&[10, 11, 9]), batch(&[])]).await;
let mut poller = LongPoller::new(client_for(&server.uri()));
poller.next_batch().await.expect("a batch arrives");
poller.next_batch().await.expect("an empty batch arrives");
assert_eq!(
offsets(&server).await[1],
Some(10),
"the offset should follow the batch's last element"
);
}
async fn poll_until_fatal(dispatcher: Dispatcher) -> Result<(), BotError> {
tokio::time::timeout(Duration::from_secs(10), dispatcher.polling())
.await
.expect("polling should have hit its fatal branch and returned")
}
#[tokio::test]
async fn flood_control_during_polling_waits_and_continues() {
let server = script(vec![
json!({
"ok": false, "error_code": 429,
"description": "Too Many Requests: retry after 0",
"parameters": { "retry_after": 0 }
}),
json!({ "ok": false, "error_code": 400, "description": "Bad Request: fatal" }),
])
.await;
let dispatcher = Dispatcher::builder(client_without_retries(&server.uri())).build();
let outcome = poll_until_fatal(dispatcher).await;
assert!(outcome.is_err(), "the fatal error should end the loop");
assert_eq!(
server.received_requests().await.unwrap().len(),
2,
"polling should have waited out the rate limit and polled again, then \
stopped on the fatal error"
);
}
#[tokio::test]
async fn a_fatal_error_ends_the_polling_loop() {
let server = script(vec![json!({
"ok": false, "error_code": 401, "description": "Unauthorized"
})])
.await;
let dispatcher = Dispatcher::builder(client_without_retries(&server.uri())).build();
let outcome = poll_until_fatal(dispatcher).await;
match outcome {
Err(BotError::Api(rustigram_api::Error::Api { error_code, .. })) => {
assert_eq!(error_code, 401);
}
other => panic!("expected the 401 to surface, got {other:?}"),
}
assert_eq!(
server.received_requests().await.unwrap().len(),
1,
"a fatal error must not be retried"
);
}
#[tokio::test]
async fn a_decode_error_is_fatal() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path_regex(r".*/getUpdates$"))
.respond_with(ResponseTemplate::new(200).set_body_string("<html>gateway</html>"))
.mount(&server)
.await;
let dispatcher = Dispatcher::builder(client_for(&server.uri())).build();
let outcome = poll_until_fatal(dispatcher).await;
assert!(
matches!(outcome, Err(BotError::Api(rustigram_api::Error::Decode(_)))),
"a body that cannot be decoded must end the loop, got {outcome:?}"
);
assert_eq!(
server.received_requests().await.unwrap().len(),
1,
"a decode error must not be retried"
);
}
#[tokio::test]
async fn polled_updates_reach_a_handler() {
let server = script(vec![
batch(&[10]),
json!({ "ok": false, "error_code": 401, "description": "Unauthorized" }),
])
.await;
let (tx, mut rx) = reports();
let dispatcher = Dispatcher::builder(client_for(&server.uri()))
.on(filters::message(), reporting(&tx, "dispatched"))
.build();
let _ = poll_until_fatal(dispatcher).await;
let arrived = tokio::time::timeout(Duration::from_secs(5), rx.recv())
.await
.expect("a handler should have run within five seconds");
assert_eq!(
arrived,
Some("dispatched"),
"the polled update never reached a handler"
);
}