tele 0.1.19

Ergonomic Telegram Bot API SDK for Rust, built on reqx
Documentation
#![cfg(feature = "axum")]

use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};

use axum::body::Bytes;
use axum::extract::State;
use axum::http::{HeaderMap, HeaderValue, StatusCode};
use serde_json::json;
use tele::bot::axum::{
    TELEGRAM_SECRET_HEADER, dispatch_webhook, dispatch_webhook_status, telegram_secret_token,
    webhook_handler,
};
use tele::bot::{BotContext, DispatchOutcome, HandlerError, Router, WebhookRunner};
use tele::types::update::Update;
use tele::{Client, Error};

type DynError = Box<dyn std::error::Error>;

fn build_client() -> Result<Client, DynError> {
    let client = Client::builder("http://127.0.0.1:9")?
        .bot_token("123:abc")?
        .build()?;
    Ok(client)
}

fn update_payload(text: &str) -> Result<Vec<u8>, DynError> {
    let payload = serde_json::to_vec(&json!({
        "update_id": 42,
        "message": {
            "message_id": 10,
            "date": 1710000000,
            "chat": {"id": 100, "type": "private"},
            "text": text
        }
    }))?;
    Ok(payload)
}

fn secret_headers(secret: &str) -> Result<HeaderMap, DynError> {
    let mut headers = HeaderMap::new();
    headers.insert(
        TELEGRAM_SECRET_HEADER,
        HeaderValue::from_str(secret).map_err(|error| format!("invalid secret header: {error}"))?,
    );
    Ok(headers)
}

#[tokio::test]
async fn dispatch_webhook_runs_router_handler() -> Result<(), DynError> {
    let client = build_client()?;
    let handler_hits = Arc::new(AtomicUsize::new(0));

    let mut router = Router::new();
    {
        let handler_hits = Arc::clone(&handler_hits);
        router
            .command_route("start")
            .handle(move |_context: BotContext, _update: Update| {
                let handler_hits = Arc::clone(&handler_hits);
                async move {
                    handler_hits.fetch_add(1, Ordering::SeqCst);
                    Ok(())
                }
            });
    }

    let runner = WebhookRunner::new(client, router).expected_secret_token("secret");
    let payload = update_payload("/start hello")?;
    let headers = secret_headers("secret")?;

    let outcome = dispatch_webhook(&runner, &headers, &payload).await?;
    assert_eq!(outcome, DispatchOutcome::Handled { update_id: 42 });
    assert_eq!(handler_hits.load(Ordering::SeqCst), 1);

    Ok(())
}

#[tokio::test]
async fn dispatch_webhook_status_maps_secret_and_json_errors() -> Result<(), DynError> {
    let client = build_client()?;
    let runner = WebhookRunner::new(client, Router::new()).expected_secret_token("secret");

    let wrong_headers = secret_headers("wrong")?;
    let payload = update_payload("hello")?;
    let unauthorized = dispatch_webhook_status(&runner, &wrong_headers, &payload).await;
    assert_eq!(unauthorized, StatusCode::UNAUTHORIZED);

    let good_headers = secret_headers("secret")?;
    let bad_payload = br#"{"update_id":"invalid"}"#;
    let bad_request = dispatch_webhook_status(&runner, &good_headers, bad_payload).await;
    assert_eq!(bad_request, StatusCode::BAD_REQUEST);

    Ok(())
}

#[tokio::test]
async fn dispatch_webhook_status_maps_handler_error_to_500() -> Result<(), DynError> {
    let client = build_client()?;
    let mut router = Router::new();
    router
        .message_route()
        .handle(|_context: BotContext, _update: Update| async move {
            Err(HandlerError::internal(Error::InvalidRequest {
                reason: "handler failed".to_owned(),
            }))
        });

    let runner = WebhookRunner::new(client, router);
    let payload = update_payload("hello")?;
    let headers = HeaderMap::new();

    let status = dispatch_webhook_status(&runner, &headers, &payload).await;
    assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);

    Ok(())
}

#[tokio::test]
async fn webhook_handler_works_with_axum_state() -> Result<(), DynError> {
    let client = build_client()?;
    let mut router = Router::new();
    router
        .message_route()
        .handle(|_context: BotContext, _update: Update| async move { Ok(()) });

    let runner = Arc::new(WebhookRunner::new(client, router));
    let payload = update_payload("hello")?;
    let headers = HeaderMap::new();

    assert_eq!(telegram_secret_token(&headers), None);

    let status = webhook_handler(State(runner), headers, Bytes::from(payload)).await;
    assert_eq!(status, StatusCode::OK);

    Ok(())
}