use crate::channels::telegram::rich::api;
use crate::channels::telegram::rich::ast::MermaidResult;
use crate::channels::telegram::rich::mermaid::cache_put;
#[tokio::test]
async fn send_rich_markdown_id_uses_custom_api_url() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("POST", "/botTESTTOKEN/sendRichMessage")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"ok":true,"result":{"message_id":42}}"#)
.create_async()
.await;
let result = api::send_rich_markdown_id(
&server.url(),
"TESTTOKEN",
12345,
None,
"hello **world**",
None,
"test",
"-",
)
.await;
assert!(result.is_ok(), "send should succeed: {:?}", result.err());
assert_eq!(result.unwrap(), 42);
mock.assert_async().await;
}
#[tokio::test]
async fn send_rich_html_id_uses_custom_api_url() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("POST", "/botTESTTOKEN/sendRichMessage")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"ok":true,"result":{"message_id":99}}"#)
.create_async()
.await;
let result = api::send_rich_html_id(
&server.url(),
"TESTTOKEN",
67890,
None,
"<b>bold</b>",
None,
"test",
"-",
)
.await;
assert!(result.is_ok(), "send should succeed: {:?}", result.err());
assert_eq!(result.unwrap(), 99);
mock.assert_async().await;
}
#[tokio::test]
async fn edit_rich_html_uses_custom_api_url() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("POST", "/botTESTTOKEN/editMessageText")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"ok":true,"result":true}"#)
.create_async()
.await;
let result = api::edit_rich_html(
&server.url(),
"TESTTOKEN",
12345,
1,
"<b>edited</b>",
None,
"test",
"-",
)
.await;
assert!(result.is_ok(), "edit should succeed: {:?}", result.err());
mock.assert_async().await;
}
#[tokio::test]
async fn edit_rich_markdown_media_url_entry_uses_custom_api_url() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("POST", "/botTESTTOKEN/editMessageText")
.match_body(mockito::Matcher::PartialJson(serde_json::json!({
"chat_id": 12345,
"message_id": 7,
"rich_message": {
"markdown": "",
"media": [
{"id": "diag0", "media": {"type": "photo", "media": "https://mermaid.ink/img/abc"}}
]
}
})))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"ok":true,"result":true}"#)
.create_async()
.await;
let kb = serde_json::json!({"inline_keyboard": [[{"text": "b", "callback_data": "cb0"}]]});
let result = api::edit_rich_markdown_media(
&server.url(),
"TESTTOKEN",
12345,
7,
"",
&[crate::channels::telegram::rich::mermaid::MediaEntry {
id: "diag0".to_string(),
url: Some("https://mermaid.ink/img/abc".to_string()),
bytes: None,
kind: crate::channels::telegram::rich::mermaid::MediaKind::Photo,
name: None,
}],
Some(&kb),
"test",
"-",
)
.await;
assert!(
result.is_ok(),
"media edit should succeed: {:?}",
result.err()
);
mock.assert_async().await;
}
#[tokio::test]
async fn send_rich_markdown_media_target_id_uses_custom_api_url() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("POST", "/botTESTTOKEN/sendRichMessage")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"ok":true,"result":{"message_id":77}}"#)
.create_async()
.await;
let result = api::send_rich_markdown_media_target_id(
&server.url(),
"TESTTOKEN",
11111,
None,
None,
"",
&[crate::channels::telegram::rich::mermaid::MediaEntry {
id: "1".to_string(),
url: Some("https://example.com/img.png".to_string()),
bytes: None,
kind: crate::channels::telegram::rich::mermaid::MediaKind::Photo,
name: None,
}],
"test",
"-",
)
.await;
assert!(
result.is_ok(),
"media send should succeed: {:?}",
result.err()
);
assert_eq!(result.unwrap(), 77);
mock.assert_async().await;
}
#[tokio::test]
async fn a_base_with_a_trailing_slash_does_not_double_the_separator() {
let mut server = mockito::Server::new_async().await;
let base_with_slash = format!("{}/", server.url());
let hit = server
.mock("POST", "/botTOKEN/sendRichMessage")
.with_status(200)
.with_body(r#"{"ok":true,"result":{"message_id":1}}"#)
.create_async()
.await;
let _ = api::send_rich_html_id(
&base_with_slash,
"TOKEN",
123,
None,
"<b>hi</b>",
None,
"test",
"-",
)
.await;
hit.assert_async().await;
}
#[tokio::test]
async fn a_base_without_a_trailing_slash_still_works() {
let mut server = mockito::Server::new_async().await;
let hit = server
.mock("POST", "/botTOKEN/sendRichMessage")
.with_status(200)
.with_body(r#"{"ok":true,"result":{"message_id":1}}"#)
.create_async()
.await;
let _ = api::send_rich_html_id(
&server.url(),
"TOKEN",
123,
None,
"<b>hi</b>",
None,
"test",
"-",
)
.await;
hit.assert_async().await;
}
#[tokio::test]
async fn the_html_fallback_wraps_each_block_in_its_own_p_tag() {
const MERMAID_FENCE_SOURCE: &str = "graph TD\n Fallback629Probe --> B";
let _guard = crate::channels::telegram::governor::test_support::registry_guard().await;
let _cooldown = crate::tests::telegram_cooldown_lock::guard().await;
let previous = crate::config::Config::current();
let mut pinned = (*previous).clone();
pinned.channels.telegram.rich_messages = true;
pinned.channels.telegram.mermaid_render = true;
crate::config::Config::set_current(pinned);
cache_put(
MERMAID_FENCE_SOURCE,
&MermaidResult::ImageBytes(png_ihdr(800, 600)),
);
let body = "First paragraph.\n\nSecond paragraph.\n\n\
```mermaid\ngraph TD\n Fallback629Probe --> B\n```";
let mut server = mockito::Server::new_async().await;
let primary = server
.mock("POST", "/botTESTTOKEN/sendRichMessage")
.match_body(mockito::Matcher::Regex(r#""media"\s*:\s*\["#.to_string()))
.with_status(400)
.with_body(r#"{"ok":false,"description":"Bad Request: can't parse rich message"}"#)
.expect(1)
.create_async()
.await;
let fallback = server
.mock("POST", "/botTESTTOKEN/sendRichMessage")
.match_body(mockito::Matcher::Regex(
r#"<p>First paragraph\.</p>"#.to_string(),
))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"ok":true,"result":{"message_id":7}}"#)
.expect(1)
.create_async()
.await;
let id = api::send_rich_with_mermaid_target_id(
&server.url(),
"TESTTOKEN",
12345,
None,
None,
body,
"test",
"-",
)
.await;
assert_eq!(
id.expect("the html fallback must deliver the message"),
7,
"the fallback leg must return its own message id"
);
fallback.assert_async().await;
primary.assert_async().await;
crate::config::Config::set_current((*previous).clone());
}
fn png_ihdr(w: u32, h: u32) -> Vec<u8> {
let mut png = vec![0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a];
png.extend_from_slice(&13u32.to_be_bytes());
png.extend_from_slice(b"IHDR");
png.extend_from_slice(&w.to_be_bytes());
png.extend_from_slice(&h.to_be_bytes());
png.extend_from_slice(&[8, 6, 0, 0, 0]);
png.extend_from_slice(&[0, 0, 0, 0]);
png
}
#[tokio::test]
async fn a_document_entry_announces_itself_as_a_document() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("POST", "/botTESTTOKEN/sendRichMessage")
.match_body(mockito::Matcher::PartialJson(serde_json::json!({
"rich_message": {
"markdown": "",
"media": [
{"id": "doc0", "media": {"type": "document", "media": "https://example.com/q3.pdf"}}
]
}
})))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"ok":true,"result":{"message_id":81}}"#)
.create_async()
.await;
let result = api::send_rich_markdown_media_target_id(
&server.url(),
"TESTTOKEN",
11111,
None,
None,
"",
&[crate::channels::telegram::rich::mermaid::MediaEntry {
id: "doc0".to_string(),
url: Some("https://example.com/q3.pdf".to_string()),
bytes: None,
kind: crate::channels::telegram::rich::mermaid::MediaKind::Document,
name: Some("q3.pdf".to_string()),
}],
"test",
"-",
)
.await;
assert!(
result.is_ok(),
"document send should succeed: {:?}",
result.err()
);
assert_eq!(result.unwrap(), 81);
mock.assert_async().await;
}
#[tokio::test]
async fn document_bytes_upload_as_a_named_multipart_part() {
let mut server = mockito::Server::new_async().await;
let mock = server
.mock("POST", "/botTESTTOKEN/sendRichMessage")
.match_body(mockito::Matcher::Regex(
r#"filename="q3\.pdf"[\s\S]*application/pdf"#.to_string(),
))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"ok":true,"result":{"message_id":82}}"#)
.create_async()
.await;
let result = api::send_rich_markdown_media_target_id(
&server.url(),
"TESTTOKEN",
11111,
None,
None,
"",
&[crate::channels::telegram::rich::mermaid::MediaEntry {
id: "doc0".to_string(),
url: None,
bytes: Some(b"%PDF-1.7\nfake bytes\n".to_vec()),
kind: crate::channels::telegram::rich::mermaid::MediaKind::Document,
name: Some("q3.pdf".to_string()),
}],
"test",
"-",
)
.await;
assert!(
result.is_ok(),
"multipart document send should succeed: {:?}",
result.err()
);
assert_eq!(result.unwrap(), 82);
mock.assert_async().await;
}
#[test]
fn document_mime_covers_the_types_a_session_produces() {
use crate::channels::telegram::rich::api::document_mime;
assert_eq!(document_mime("report.pdf"), "application/pdf");
assert_eq!(
document_mime("notes.TXT"),
"text/plain",
"extension match is case-blind"
);
assert_eq!(document_mime("events.json"), "application/json");
assert_eq!(document_mime("main.rs"), "text/rust");
assert_eq!(document_mime("lib.py"), "text/x-python");
assert_eq!(document_mime("page.html"), "text/html");
assert_eq!(
document_mime("archive.tar.gz"),
"application/gzip",
"the last extension wins"
);
assert_eq!(
document_mime("blob"),
"application/octet-stream",
"no extension: generic"
);
}
#[test]
fn a_part_identity_follows_the_entry_kind() {
use crate::channels::telegram::rich::api::media_part_identity;
use crate::channels::telegram::rich::mermaid::{MediaEntry, MediaKind};
let photo = MediaEntry {
id: "d1".to_string(),
url: None,
bytes: Some(Vec::new()),
kind: MediaKind::Photo,
name: None,
};
assert_eq!(
media_part_identity(&photo),
("d1.png".to_string(), "image/png")
);
let named = MediaEntry {
id: "doc0".to_string(),
url: None,
bytes: Some(Vec::new()),
kind: MediaKind::Document,
name: Some("q3.pdf".to_string()),
};
assert_eq!(
media_part_identity(&named),
("q3.pdf".to_string(), "application/pdf")
);
let unnamed = MediaEntry {
id: "doc1".to_string(),
url: None,
bytes: Some(Vec::new()),
kind: MediaKind::Document,
name: None,
};
assert_eq!(
media_part_identity(&unnamed),
("doc1.bin".to_string(), "application/octet-stream")
);
}