//! Round26 raw integration coverage for high-yield channel cold paths. //! //! Loopback Bot API endpoints and parser/codec fixtures only: no real channel //! network services are contacted. use axum::{ body::Bytes, extract::State, http::{HeaderMap, StatusCode}, routing::post, Json, Router, }; use openhuman_core::openhuman::channels::providers::email_channel::{ test_support as email_support, EmailChannel, EmailConfig, }; use openhuman_core::openhuman::channels::providers::irc::test_support as irc_support; use openhuman_core::openhuman::channels::providers::telegram::TelegramChannel; use openhuman_core::openhuman::channels::providers::yuanbao::{ proto::decode_conn_msg, proto_biz::{ decode_biz_rsp_code, decode_get_group_member_list_rsp, decode_query_group_info_rsp, decode_response_envelope, encode_get_group_member_list, encode_query_group_info, encode_send_c2c_message, encode_send_group_heartbeat, encode_send_group_message, encode_send_private_heartbeat, }, proto_constants::{biz_cmd, cmd_type, module, ws_heartbeat}, types::{MsgBodyElement, MsgContent}, wire::{encode_field_bytes, encode_field_string, encode_field_varint}, }; use openhuman_core::openhuman::channels::traits::{Channel, SendMessage}; use serde_json::{json, Value}; use std::sync::{Arc, Mutex}; #[derive(Default)] struct TelegramMockState { send_message_calls: Mutex>, reaction_calls: Mutex>, json_media_calls: Mutex>, multipart_calls: Mutex>, } async fn spawn_telegram_mock() -> (String, Arc) { let state = Arc::new(TelegramMockState::default()); let app = Router::new() .route("/botround26/sendMessage", post(telegram_send_message)) .route( "/botround26/setMessageReaction", post(telegram_set_reaction), ) .route("/botround26/sendDocument", post(telegram_media)) .route("/botround26/sendPhoto", post(telegram_media)) .route("/botround26/sendVideo", post(telegram_media)) .route("/botround26/sendAudio", post(telegram_media)) .route("/botround26/sendVoice", post(telegram_media)) .with_state(Arc::clone(&state)); let listener = tokio::net::TcpListener::bind("127.0.0.1:0") .await .expect("bind telegram mock"); let addr = listener.local_addr().expect("telegram mock addr"); tokio::spawn(async move { axum::serve(listener, app) .await .expect("serve telegram mock"); }); (format!("http://127.0.0.1:{}", addr.port()), state) } async fn telegram_send_message( State(state): State>, Json(body): Json, ) -> (StatusCode, Json) { let mut calls = state.send_message_calls.lock().expect("sendMessage calls"); calls.push(body); if calls.len() == 1 { ( StatusCode::BAD_REQUEST, Json(json!({"ok": false, "description": "markdown parse failed"})), ) } else { ( StatusCode::OK, Json(json!({"ok": true, "result": {"message_id": 7}})), ) } } async fn telegram_set_reaction( State(state): State>, Json(body): Json, ) -> (StatusCode, Json) { let emoji = body .pointer("/reaction/0/emoji") .and_then(Value::as_str) .unwrap_or_default() .to_string(); state .reaction_calls .lock() .expect("reaction calls") .push(body); if emoji == "💥" { ( StatusCode::BAD_REQUEST, Json(json!({"ok": false, "description": "reaction rejected"})), ) } else { (StatusCode::OK, Json(json!({"ok": true}))) } } async fn telegram_media( State(state): State>, headers: HeaderMap, uri: axum::http::Uri, body: Bytes, ) -> (StatusCode, Json) { let method = uri .path() .rsplit('/') .next() .unwrap_or_default() .to_string(); let content_type = headers .get(axum::http::header::CONTENT_TYPE) .and_then(|value| value.to_str().ok()) .unwrap_or_default() .to_string(); if content_type.starts_with("application/json") { let value = serde_json::from_slice::(&body).expect("telegram media json"); state .json_media_calls .lock() .expect("json media calls") .push((method, value)); } else { let text = String::from_utf8_lossy(&body).to_string(); state .multipart_calls .lock() .expect("multipart calls") .push((method, text)); } ( StatusCode::OK, Json(json!({"ok": true, "result": {"message_id": 9}})), ) } struct EnvGuard { key: &'static str, prior: Option, } impl EnvGuard { fn set(key: &'static str, value: String) -> Self { let prior = std::env::var(key).ok(); std::env::set_var(key, value); Self { key, prior } } } impl Drop for EnvGuard { fn drop(&mut self) { if let Some(value) = self.prior.take() { std::env::set_var(self.key, value); } else { std::env::remove_var(self.key); } } } impl EnvGuard { fn unset(key: &'static str) -> Self { let prior = std::env::var(key).ok(); std::env::remove_var(key); Self { key, prior } } } fn env_lock() -> std::sync::MutexGuard<'static, ()> { static LOCK: std::sync::OnceLock> = std::sync::OnceLock::new(); LOCK.get_or_init(|| std::sync::Mutex::new(())) .lock() .unwrap_or_else(|e| e.into_inner()) } fn text_body(text: &str) -> Vec { vec![MsgBodyElement { msg_type: "TIMTextElem".to_string(), msg_content: MsgContent { text: Some(text.to_string()), ..Default::default() }, }] } #[tokio::test] async fn telegram_loopback_covers_reaction_text_fallback_and_media_send_paths() { let _env = env_lock(); let (base, state) = spawn_telegram_mock().await; let _guard = EnvGuard::set("OPENHUMAN_TELEGRAM_BOT_API_BASE", base); let _legacy_guard = EnvGuard::unset("OPENHUMAN_TELEGRAM_API_BASE"); let channel = TelegramChannel::new("round26".to_string(), vec!["alice".to_string()], false); channel .send(&SendMessage::new("[REACTION:👍|42]", "chat-1:topic-2")) .await .expect("reaction-only send"); channel .send(&SendMessage::new("[REACTION:💥|43]", "chat-1:topic-2")) .await .expect("failed reaction is non-fatal"); channel .send( &SendMessage::new("**markdown fallback**", "chat-1:topic-2") .in_thread(Some("41".to_string())), ) .await .expect("send text falls back to plain"); channel .send_document_by_url( "chat-1", Some("topic-2"), "https://files.example/doc.pdf", Some("doc"), ) .await .expect("document by url"); channel .send_photo_by_url("chat-1", None, "https://files.example/photo.png", None) .await .expect("photo by url"); channel .send_video_by_url( "chat-1", Some("topic-2"), "https://files.example/video.mp4", Some("video"), ) .await .expect("video by url"); channel .send_audio_by_url("chat-1", None, "https://files.example/audio.mp3", None) .await .expect("audio by url"); channel .send_voice_by_url( "chat-1", Some("topic-2"), "https://files.example/voice.ogg", Some("voice"), ) .await .expect("voice by url"); channel .send_document_bytes( "chat-1", Some("topic-2"), b"round26 document".to_vec(), "round26.txt", Some("bytes"), ) .await .expect("document bytes"); channel .send_photo_bytes( "chat-1", None, b"not really an image".to_vec(), "round26.png", Some("photo bytes"), ) .await .expect("photo bytes"); let reactions = state.reaction_calls.lock().expect("reaction calls"); assert_eq!(reactions.len(), 2); assert_eq!(reactions[0]["message_id"], 42); drop(reactions); let messages = state.send_message_calls.lock().expect("sendMessage calls"); assert_eq!(messages.len(), 2); assert_eq!(messages[0]["parse_mode"], "Markdown"); assert!(messages[1].get("parse_mode").is_none()); assert_eq!(messages[1]["message_thread_id"], "topic-2"); assert_eq!(messages[1]["reply_to_message_id"], 41); drop(messages); let json_media = state.json_media_calls.lock().expect("json media calls"); assert_eq!(json_media.len(), 5); assert_eq!(json_media[0].0, "sendDocument"); assert_eq!(json_media[0].1["document"], "https://files.example/doc.pdf"); assert_eq!(json_media[2].0, "sendVideo"); assert_eq!(json_media[4].0, "sendVoice"); drop(json_media); let multipart = state.multipart_calls.lock().expect("multipart calls"); assert_eq!(multipart.len(), 2); assert_eq!(multipart[0].0, "sendDocument"); assert!(multipart[0].1.contains("round26.txt")); assert_eq!(multipart[1].0, "sendPhoto"); assert!(multipart[1].1.contains("round26.png")); } #[test] fn irc_and_email_parser_edges_cover_helpers_without_sockets() { let parsed = irc_support::parse_line_for_test(":Alice!u@h PRIVMSG #ops :hello world") .expect("irc privmsg parse"); assert_eq!(parsed.0.as_deref(), Some("Alice!u@h")); assert_eq!(parsed.1, "PRIVMSG"); assert_eq!( parsed.2, vec!["#ops".to_string(), "hello world".to_string()] ); assert_eq!(parsed.3.as_deref(), Some("Alice")); assert_eq!( irc_support::parse_line_for_test("PING :server") .expect("ping parse") .2, vec!["server".to_string()] ); assert!(irc_support::parse_line_for_test("").is_none()); assert_eq!( irc_support::encode_sasl_plain_for_test("openhuman", "secret"), "AG9wZW5odW1hbgBzZWNyZXQ=" ); assert!(irc_support::is_user_allowed_for_test( vec!["*".to_string()], "Anyone" )); assert!(irc_support::is_user_allowed_for_test( vec!["Alice".to_string()], "alice" )); assert!(!irc_support::is_user_allowed_for_test( vec!["Alice".to_string()], "bob" )); let chunks = irc_support::split_message_for_test("alpha\nβeta\r\n0123456789", 5); assert_eq!(chunks, vec!["alpha", "βeta", "01234", "56789"]); assert_eq!( irc_support::split_message_for_test("\n", 0), vec![String::new()] ); let channel = EmailChannel::new(EmailConfig { from_address: "bot@example.test".to_string(), allowed_senders: vec![ "*".to_string(), "admin@example.test".to_string(), "@team.example".to_string(), ], ..Default::default() }); assert!(channel.is_sender_allowed("blocked@anywhere.test")); assert_eq!( EmailChannel::strip_html("
hello
team
"), "helloteam" ); let no_body = b"From: Unknown \r\nSubject: No Body\r\n\r\n"; let parsed = email_support::parse_email_fixture(no_body).expect("email parse"); assert_eq!(parsed.sender, "nobody@example.test"); assert_eq!(parsed.subject.as_deref(), Some("No Body")); assert!(parsed.text.is_empty()); let plain = channel .build_plain_message("ops@example.test", "Round26", "plain body") .expect("plain email"); let formatted = String::from_utf8_lossy(&plain.formatted()).to_string(); assert!(formatted.contains("Subject: Round26")); assert!(formatted.contains("plain body")); } #[test] fn yuanbao_biz_codecs_cover_success_error_and_optional_field_paths() { let c2c = encode_send_c2c_message( "uid_alice", "uid_bot", &text_body("hello dm"), "msg-c2c", 99, "group-from-dm", "trace-c2c", ); let c2c_frame = decode_conn_msg(&c2c).expect("decode c2c frame"); assert_eq!(c2c_frame.cmd_type, cmd_type::REQUEST); assert_eq!(c2c_frame.cmd, biz_cmd::SEND_C2C_MESSAGE); assert_eq!(c2c_frame.module, module::BIZ_PKG); assert_eq!(c2c_frame.msg_id, "msg-c2c"); let group = encode_send_group_message( "group-1", "uid_bot", &text_body("hello group"), "msg-group", "uid-target", "random-1", "ref-1", "trace-group", ); let group_frame = decode_conn_msg(&group).expect("decode group frame"); assert_eq!(group_frame.cmd, biz_cmd::SEND_GROUP_MESSAGE); assert_eq!(group_frame.msg_id, "msg-group"); let private_hb = encode_send_private_heartbeat("hb-private", "uid_bot", "uid_alice", ws_heartbeat::RUNNING); assert_eq!( decode_conn_msg(&private_hb).expect("private hb").cmd, biz_cmd::SEND_PRIVATE_HEARTBEAT ); let group_hb = encode_send_group_heartbeat( "hb-group", "uid_bot", "group-1", ws_heartbeat::RUNNING, 1_700_000_001, ); assert_eq!( decode_conn_msg(&group_hb).expect("group hb").cmd, biz_cmd::SEND_GROUP_HEARTBEAT ); let group_info_req = encode_query_group_info("q-info", "group-1"); assert_eq!( decode_response_envelope(&group_info_req) .expect("query group envelope") .cmd, biz_cmd::QUERY_GROUP_INFO ); let member_req = encode_get_group_member_list("q-members", "group-1", 20, 10); assert_eq!( decode_conn_msg(&member_req).expect("member req").cmd, biz_cmd::GET_GROUP_MEMBER_LIST ); let mut group_inner = Vec::new(); encode_field_string(1, "Round26 Group", &mut group_inner); encode_field_string(2, "owner-id", &mut group_inner); encode_field_string(3, "Owner", &mut group_inner); encode_field_varint(4, 3, &mut group_inner); let mut group_rsp = Vec::new(); encode_field_varint(1, 0, &mut group_rsp); encode_field_string(2, "ok", &mut group_rsp); encode_field_bytes(3, &group_inner, &mut group_rsp); let group_info = decode_query_group_info_rsp(&group_rsp).expect("group info rsp"); assert_eq!(group_info.group_name, "Round26 Group"); assert_eq!(group_info.member_count, 3); let mut member = Vec::new(); encode_field_string(1, "uid-member", &mut member); encode_field_string(2, "Member", &mut member); encode_field_varint(3, 2, &mut member); encode_field_varint(4, 1_700_000_000, &mut member); encode_field_string(5, "Card", &mut member); let mut members_rsp = Vec::new(); encode_field_varint(1, 0, &mut members_rsp); encode_field_string(2, "ok", &mut members_rsp); encode_field_bytes(3, &member, &mut members_rsp); encode_field_varint(4, 30, &mut members_rsp); encode_field_varint(5, 1, &mut members_rsp); let page = decode_get_group_member_list_rsp(&members_rsp).expect("members rsp"); assert_eq!(page.members[0].user_id, "uid-member"); assert_eq!(page.members[0].name_card, "Card"); assert_eq!(page.next_offset, 30); assert!(page.is_complete); let mut error_rsp = Vec::new(); encode_field_varint(1, 4002, &mut error_rsp); encode_field_string(2, "rate limited", &mut error_rsp); assert_eq!( decode_biz_rsp_code(&error_rsp).expect("biz code"), (4002, "rate limited".to_string()) ); let mut overflow = Vec::new(); encode_field_varint(1, u64::MAX, &mut overflow); assert!(decode_biz_rsp_code(&overflow) .expect_err("overflow rejected") .to_string() .contains("out of i32 range")); }