Some checks failed
CI / Check (pull_request) Has been cancelled
CI / Format (pull_request) Has been cancelled
CI / Clippy (pull_request) Has been cancelled
CI / Build (macos-latest) (pull_request) Has been cancelled
CI / Build (ubuntu-latest) (pull_request) Has been cancelled
CI / Build (windows-latest) (pull_request) Has been cancelled
Comprehensive cleanup of unused methods, dead code, and compiler warnings:
## Removed Methods (15):
- Duplicate delegation methods: is_authenticated, get_typing_text, get_user_name from TdClient
- Obsolete methods: TdClient::init() (duplicated in main.rs)
- Unused getters: UserCache::{get_username, get_name, get_user_id_by_chat}
- Unused builder methods: MessageBuilder::{edited, add_reaction}
- Unused utility: ChatState::is_normal()
- Dead code: HotkeysConfig::{matches, key_matches} (kept for tests)
- Unused method: UserCache::register_private_chat()
- Getter replaced with direct field access: MessageInfo::edit_date()
## Removed Module:
- error.rs - Unused error handling module (TeletuiError, ErrorVariant, IntoTeletuiError)
## Removed Constants (8):
- EMOJI_PICKER_COLUMNS, EMOJI_PICKER_ROWS, MAX_INPUT_HEIGHT
- MIN_TERMINAL_WIDTH, MIN_TERMINAL_HEIGHT
- TDLIB_CHAT_LIMIT, MAX_USERNAME_DISPLAY_LENGTH, MESSAGE_TEXT_INDENT
## Fixed Warnings:
- Removed unused imports (8 instances)
- Fixed unreachable patterns (10 instances)
- Fixed irrefutable if let patterns (2 instances)
- Fixed unused variables (1 instance)
- Removed dead_code annotations where appropriate
## Improvements:
- Integrated Config::load_credentials() into TdClient::new() for better credential management
- Replaced edit_date() getter with direct field access (message.metadata.edit_date)
- Updated tests to use direct field access instead of removed getters
## Test Results:
All tests passing: 499 passed, 0 failed
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
895 lines
31 KiB
Rust
895 lines
31 KiB
Rust
// Fake TDLib client for testing
|
||
|
||
use std::collections::HashMap;
|
||
use std::sync::{Arc, Mutex};
|
||
use tele_tui::tdlib::{ChatInfo, MessageInfo, NetworkState, ProfileInfo, ReplyInfo};
|
||
use tele_tui::tdlib::types::{FolderInfo, ReactionInfo};
|
||
use tele_tui::types::{ChatId, MessageId, UserId};
|
||
use tokio::sync::mpsc;
|
||
|
||
/// Update события от TDLib (упрощённая версия)
|
||
#[derive(Debug, Clone)]
|
||
pub enum TdUpdate {
|
||
NewMessage { chat_id: ChatId, message: MessageInfo },
|
||
MessageContent { chat_id: ChatId, message_id: MessageId, new_text: String },
|
||
DeleteMessages { chat_id: ChatId, message_ids: Vec<MessageId> },
|
||
ChatAction { chat_id: ChatId, user_id: UserId, action: String },
|
||
MessageInteractionInfo { chat_id: ChatId, message_id: MessageId, reactions: Vec<ReactionInfo> },
|
||
ConnectionState { state: NetworkState },
|
||
ChatReadOutbox { chat_id: ChatId, last_read_outbox_message_id: MessageId },
|
||
ChatDraftMessage { chat_id: ChatId, draft_text: Option<String> },
|
||
}
|
||
|
||
/// Упрощённый mock TDLib клиента для тестов
|
||
pub struct FakeTdClient {
|
||
// Данные
|
||
pub chats: Arc<Mutex<Vec<ChatInfo>>>,
|
||
pub messages: Arc<Mutex<HashMap<i64, Vec<MessageInfo>>>>,
|
||
pub folders: Arc<Mutex<Vec<FolderInfo>>>,
|
||
pub user_names: Arc<Mutex<HashMap<i64, String>>>,
|
||
pub profiles: Arc<Mutex<HashMap<i64, ProfileInfo>>>,
|
||
pub drafts: Arc<Mutex<HashMap<i64, String>>>,
|
||
pub available_reactions: Arc<Mutex<Vec<String>>>,
|
||
|
||
// Состояние
|
||
pub network_state: Arc<Mutex<NetworkState>>,
|
||
pub typing_chat_id: Arc<Mutex<Option<i64>>>,
|
||
pub current_chat_id: Arc<Mutex<Option<i64>>>,
|
||
|
||
// История действий (для проверки в тестах)
|
||
pub sent_messages: Arc<Mutex<Vec<SentMessage>>>,
|
||
pub edited_messages: Arc<Mutex<Vec<EditedMessage>>>,
|
||
pub deleted_messages: Arc<Mutex<Vec<DeletedMessages>>>,
|
||
pub forwarded_messages: Arc<Mutex<Vec<ForwardedMessages>>>,
|
||
pub searched_queries: Arc<Mutex<Vec<SearchQuery>>>,
|
||
pub viewed_messages: Arc<Mutex<Vec<(i64, Vec<i64>)>>>, // (chat_id, message_ids)
|
||
pub chat_actions: Arc<Mutex<Vec<(i64, String)>>>, // (chat_id, action)
|
||
|
||
// Update channel для симуляции событий
|
||
pub update_tx: Arc<Mutex<Option<mpsc::UnboundedSender<TdUpdate>>>>,
|
||
|
||
// Настройки поведения
|
||
pub simulate_delays: bool,
|
||
pub fail_next_operation: Arc<Mutex<bool>>,
|
||
}
|
||
|
||
#[derive(Debug, Clone)]
|
||
pub struct SentMessage {
|
||
pub chat_id: i64,
|
||
pub text: String,
|
||
pub reply_to: Option<MessageId>,
|
||
pub reply_info: Option<ReplyInfo>,
|
||
}
|
||
|
||
#[derive(Debug, Clone)]
|
||
pub struct EditedMessage {
|
||
pub chat_id: i64,
|
||
pub message_id: MessageId,
|
||
pub new_text: String,
|
||
}
|
||
|
||
#[derive(Debug, Clone)]
|
||
pub struct DeletedMessages {
|
||
pub chat_id: i64,
|
||
pub message_ids: Vec<MessageId>,
|
||
pub revoke: bool,
|
||
}
|
||
|
||
#[derive(Debug, Clone)]
|
||
pub struct ForwardedMessages {
|
||
pub from_chat_id: i64,
|
||
pub to_chat_id: i64,
|
||
pub message_ids: Vec<MessageId>,
|
||
}
|
||
|
||
#[derive(Debug, Clone)]
|
||
pub struct SearchQuery {
|
||
pub chat_id: i64,
|
||
pub query: String,
|
||
pub results_count: usize,
|
||
}
|
||
|
||
impl Default for FakeTdClient {
|
||
fn default() -> Self {
|
||
Self::new()
|
||
}
|
||
}
|
||
|
||
impl Clone for FakeTdClient {
|
||
fn clone(&self) -> Self {
|
||
Self {
|
||
chats: Arc::clone(&self.chats),
|
||
messages: Arc::clone(&self.messages),
|
||
folders: Arc::clone(&self.folders),
|
||
user_names: Arc::clone(&self.user_names),
|
||
profiles: Arc::clone(&self.profiles),
|
||
drafts: Arc::clone(&self.drafts),
|
||
available_reactions: Arc::clone(&self.available_reactions),
|
||
network_state: Arc::clone(&self.network_state),
|
||
typing_chat_id: Arc::clone(&self.typing_chat_id),
|
||
current_chat_id: Arc::clone(&self.current_chat_id),
|
||
sent_messages: Arc::clone(&self.sent_messages),
|
||
edited_messages: Arc::clone(&self.edited_messages),
|
||
deleted_messages: Arc::clone(&self.deleted_messages),
|
||
forwarded_messages: Arc::clone(&self.forwarded_messages),
|
||
searched_queries: Arc::clone(&self.searched_queries),
|
||
viewed_messages: Arc::clone(&self.viewed_messages),
|
||
chat_actions: Arc::clone(&self.chat_actions),
|
||
update_tx: Arc::clone(&self.update_tx),
|
||
simulate_delays: self.simulate_delays,
|
||
fail_next_operation: Arc::clone(&self.fail_next_operation),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl FakeTdClient {
|
||
pub fn new() -> Self {
|
||
Self {
|
||
chats: Arc::new(Mutex::new(vec![])),
|
||
messages: Arc::new(Mutex::new(HashMap::new())),
|
||
folders: Arc::new(Mutex::new(vec![FolderInfo { id: 0, name: "All".to_string() }])),
|
||
user_names: Arc::new(Mutex::new(HashMap::new())),
|
||
profiles: Arc::new(Mutex::new(HashMap::new())),
|
||
drafts: Arc::new(Mutex::new(HashMap::new())),
|
||
available_reactions: Arc::new(Mutex::new(vec![
|
||
"👍".to_string(), "❤️".to_string(), "😂".to_string(), "😮".to_string(),
|
||
"😢".to_string(), "🙏".to_string(), "👏".to_string(), "🔥".to_string(),
|
||
])),
|
||
network_state: Arc::new(Mutex::new(NetworkState::Ready)),
|
||
typing_chat_id: Arc::new(Mutex::new(None)),
|
||
current_chat_id: Arc::new(Mutex::new(None)),
|
||
sent_messages: Arc::new(Mutex::new(vec![])),
|
||
edited_messages: Arc::new(Mutex::new(vec![])),
|
||
deleted_messages: Arc::new(Mutex::new(vec![])),
|
||
forwarded_messages: Arc::new(Mutex::new(vec![])),
|
||
searched_queries: Arc::new(Mutex::new(vec![])),
|
||
viewed_messages: Arc::new(Mutex::new(vec![])),
|
||
chat_actions: Arc::new(Mutex::new(vec![])),
|
||
update_tx: Arc::new(Mutex::new(None)),
|
||
simulate_delays: false,
|
||
fail_next_operation: Arc::new(Mutex::new(false)),
|
||
}
|
||
}
|
||
|
||
/// Создать update channel для получения событий
|
||
pub fn with_update_channel(self) -> (Self, mpsc::UnboundedReceiver<TdUpdate>) {
|
||
let (tx, rx) = mpsc::unbounded_channel();
|
||
*self.update_tx.lock().unwrap() = Some(tx);
|
||
(self, rx)
|
||
}
|
||
|
||
/// Включить симуляцию задержек (как в реальном TDLib)
|
||
pub fn with_delays(mut self) -> Self {
|
||
self.simulate_delays = true;
|
||
self
|
||
}
|
||
|
||
// ==================== Builder Methods ====================
|
||
|
||
/// Добавить чат
|
||
pub fn with_chat(self, chat: ChatInfo) -> Self {
|
||
self.chats.lock().unwrap().push(chat);
|
||
self
|
||
}
|
||
|
||
/// Добавить несколько чатов
|
||
pub fn with_chats(self, chats: Vec<ChatInfo>) -> Self {
|
||
self.chats.lock().unwrap().extend(chats);
|
||
self
|
||
}
|
||
|
||
/// Добавить сообщение в чат
|
||
pub fn with_message(self, chat_id: i64, message: MessageInfo) -> Self {
|
||
self.messages
|
||
.lock()
|
||
.unwrap()
|
||
.entry(chat_id)
|
||
.or_insert_with(Vec::new)
|
||
.push(message);
|
||
self
|
||
}
|
||
|
||
/// Добавить несколько сообщений в чат
|
||
pub fn with_messages(self, chat_id: i64, messages: Vec<MessageInfo>) -> Self {
|
||
self.messages
|
||
.lock()
|
||
.unwrap()
|
||
.insert(chat_id, messages);
|
||
self
|
||
}
|
||
|
||
/// Добавить папку
|
||
pub fn with_folder(self, id: i32, name: &str) -> Self {
|
||
self.folders.lock().unwrap().push(FolderInfo { id, name: name.to_string() });
|
||
self
|
||
}
|
||
|
||
/// Добавить пользователя
|
||
pub fn with_user(self, id: i64, name: &str) -> Self {
|
||
self.user_names.lock().unwrap().insert(id, name.to_string());
|
||
self
|
||
}
|
||
|
||
/// Добавить профиль
|
||
pub fn with_profile(self, chat_id: i64, profile: ProfileInfo) -> Self {
|
||
self.profiles.lock().unwrap().insert(chat_id, profile);
|
||
self
|
||
}
|
||
|
||
/// Установить состояние сети
|
||
pub fn with_network_state(self, state: NetworkState) -> Self {
|
||
*self.network_state.lock().unwrap() = state;
|
||
self
|
||
}
|
||
|
||
/// Установить доступные реакции
|
||
pub fn with_available_reactions(self, reactions: Vec<String>) -> Self {
|
||
*self.available_reactions.lock().unwrap() = reactions;
|
||
self
|
||
}
|
||
|
||
// ==================== Async TDLib Operations ====================
|
||
|
||
/// Загрузить список чатов
|
||
pub async fn load_chats(&self, limit: usize) -> Result<Vec<ChatInfo>, String> {
|
||
if self.should_fail() {
|
||
return Err("Failed to load chats".to_string());
|
||
}
|
||
|
||
if self.simulate_delays {
|
||
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
||
}
|
||
|
||
let chats = self.chats.lock().unwrap().iter().take(limit).cloned().collect();
|
||
Ok(chats)
|
||
}
|
||
|
||
/// Открыть чат
|
||
pub async fn open_chat(&self, chat_id: ChatId) -> Result<(), String> {
|
||
if self.should_fail() {
|
||
return Err("Failed to open chat".to_string());
|
||
}
|
||
|
||
*self.current_chat_id.lock().unwrap() = Some(chat_id.as_i64());
|
||
Ok(())
|
||
}
|
||
|
||
/// Получить историю чата
|
||
pub async fn get_chat_history(&self, chat_id: ChatId, limit: i32) -> Result<Vec<MessageInfo>, String> {
|
||
if self.should_fail() {
|
||
return Err("Failed to load history".to_string());
|
||
}
|
||
|
||
if self.simulate_delays {
|
||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||
}
|
||
|
||
let messages = self.messages
|
||
.lock()
|
||
.unwrap()
|
||
.get(&chat_id.as_i64())
|
||
.map(|msgs| msgs.iter().take(limit as usize).cloned().collect())
|
||
.unwrap_or_default();
|
||
|
||
Ok(messages)
|
||
}
|
||
|
||
/// Загрузить старые сообщения
|
||
pub async fn load_older_messages(&self, chat_id: ChatId, from_message_id: MessageId) -> Result<Vec<MessageInfo>, String> {
|
||
if self.should_fail() {
|
||
return Err("Failed to load older messages".to_string());
|
||
}
|
||
|
||
let messages = self.messages.lock().unwrap();
|
||
let chat_messages = messages.get(&chat_id.as_i64()).ok_or("Chat not found")?;
|
||
|
||
// Найти индекс сообщения и вернуть предыдущие
|
||
if let Some(idx) = chat_messages.iter().position(|m| m.id() == from_message_id) {
|
||
let older: Vec<_> = chat_messages.iter().take(idx).cloned().collect();
|
||
Ok(older)
|
||
} else {
|
||
Ok(vec![])
|
||
}
|
||
}
|
||
|
||
/// Отправить сообщение
|
||
pub async fn send_message(
|
||
&self,
|
||
chat_id: ChatId,
|
||
text: String,
|
||
reply_to: Option<MessageId>,
|
||
reply_info: Option<ReplyInfo>,
|
||
) -> Result<MessageInfo, String> {
|
||
if self.should_fail() {
|
||
return Err("Failed to send message".to_string());
|
||
}
|
||
|
||
if self.simulate_delays {
|
||
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
|
||
}
|
||
|
||
let message_id = MessageId::new((self.sent_messages.lock().unwrap().len() as i64) + 1000);
|
||
|
||
self.sent_messages.lock().unwrap().push(SentMessage {
|
||
chat_id: chat_id.as_i64(),
|
||
text: text.clone(),
|
||
reply_to,
|
||
reply_info: reply_info.clone(),
|
||
});
|
||
|
||
let message = MessageInfo::new(
|
||
message_id,
|
||
"You".to_string(),
|
||
true, // is_outgoing
|
||
text.clone(),
|
||
vec![], // entities
|
||
chrono::Utc::now().timestamp() as i32,
|
||
0,
|
||
false, // is_read (станет true после Update)
|
||
true, // can_be_edited
|
||
true, // can_be_deleted_only_for_self
|
||
true, // can_be_deleted_for_all_users
|
||
reply_info,
|
||
None, // forward_from
|
||
vec![], // reactions
|
||
);
|
||
|
||
// Добавляем в историю
|
||
self.messages
|
||
.lock()
|
||
.unwrap()
|
||
.entry(chat_id.as_i64())
|
||
.or_insert_with(Vec::new)
|
||
.push(message.clone());
|
||
|
||
// Отправляем Update::NewMessage
|
||
self.send_update(TdUpdate::NewMessage {
|
||
chat_id,
|
||
message: message.clone(),
|
||
});
|
||
|
||
Ok(message)
|
||
}
|
||
|
||
/// Редактировать сообщение
|
||
pub async fn edit_message(
|
||
&self,
|
||
chat_id: ChatId,
|
||
message_id: MessageId,
|
||
new_text: String,
|
||
) -> Result<MessageInfo, String> {
|
||
if self.should_fail() {
|
||
return Err("Failed to edit message".to_string());
|
||
}
|
||
|
||
if self.simulate_delays {
|
||
tokio::time::sleep(tokio::time::Duration::from_millis(150)).await;
|
||
}
|
||
|
||
self.edited_messages.lock().unwrap().push(EditedMessage {
|
||
chat_id: chat_id.as_i64(),
|
||
message_id,
|
||
new_text: new_text.clone(),
|
||
});
|
||
|
||
// Обновляем сообщение
|
||
let mut messages = self.messages.lock().unwrap();
|
||
if let Some(chat_msgs) = messages.get_mut(&chat_id.as_i64()) {
|
||
if let Some(msg) = chat_msgs.iter_mut().find(|m| m.id() == message_id) {
|
||
msg.content.text = new_text.clone();
|
||
msg.metadata.edit_date = msg.metadata.date + 60;
|
||
|
||
let updated = msg.clone();
|
||
drop(messages); // Освобождаем lock перед отправкой update
|
||
|
||
// Отправляем Update
|
||
self.send_update(TdUpdate::MessageContent {
|
||
chat_id,
|
||
message_id,
|
||
new_text,
|
||
});
|
||
|
||
return Ok(updated);
|
||
}
|
||
}
|
||
|
||
Err("Message not found".to_string())
|
||
}
|
||
|
||
/// Удалить сообщения
|
||
pub async fn delete_messages(
|
||
&self,
|
||
chat_id: ChatId,
|
||
message_ids: Vec<MessageId>,
|
||
revoke: bool,
|
||
) -> Result<(), String> {
|
||
if self.should_fail() {
|
||
return Err("Failed to delete messages".to_string());
|
||
}
|
||
|
||
if self.simulate_delays {
|
||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||
}
|
||
|
||
self.deleted_messages.lock().unwrap().push(DeletedMessages {
|
||
chat_id: chat_id.as_i64(),
|
||
message_ids: message_ids.clone(),
|
||
revoke,
|
||
});
|
||
|
||
// Удаляем из истории
|
||
let mut messages = self.messages.lock().unwrap();
|
||
if let Some(chat_msgs) = messages.get_mut(&chat_id.as_i64()) {
|
||
chat_msgs.retain(|m| !message_ids.contains(&m.id()));
|
||
}
|
||
drop(messages);
|
||
|
||
// Отправляем Update
|
||
self.send_update(TdUpdate::DeleteMessages {
|
||
chat_id,
|
||
message_ids,
|
||
});
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Переслать сообщения
|
||
pub async fn forward_messages(
|
||
&self,
|
||
to_chat_id: ChatId,
|
||
from_chat_id: ChatId,
|
||
message_ids: Vec<MessageId>,
|
||
) -> Result<(), String> {
|
||
if self.should_fail() {
|
||
return Err("Failed to forward messages".to_string());
|
||
}
|
||
|
||
if self.simulate_delays {
|
||
tokio::time::sleep(tokio::time::Duration::from_millis(150)).await;
|
||
}
|
||
|
||
self.forwarded_messages.lock().unwrap().push(ForwardedMessages {
|
||
from_chat_id: from_chat_id.as_i64(),
|
||
to_chat_id: to_chat_id.as_i64(),
|
||
message_ids,
|
||
});
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Поиск сообщений в чате
|
||
pub async fn search_messages(&self, chat_id: ChatId, query: &str) -> Result<Vec<MessageInfo>, String> {
|
||
if self.should_fail() {
|
||
return Err("Failed to search messages".to_string());
|
||
}
|
||
|
||
let messages = self.messages.lock().unwrap();
|
||
let results: Vec<_> = messages
|
||
.get(&chat_id.as_i64())
|
||
.map(|msgs| {
|
||
msgs.iter()
|
||
.filter(|m| m.text().to_lowercase().contains(&query.to_lowercase()))
|
||
.cloned()
|
||
.collect()
|
||
})
|
||
.unwrap_or_default();
|
||
|
||
self.searched_queries.lock().unwrap().push(SearchQuery {
|
||
chat_id: chat_id.as_i64(),
|
||
query: query.to_string(),
|
||
results_count: results.len(),
|
||
});
|
||
|
||
Ok(results)
|
||
}
|
||
|
||
/// Установить черновик
|
||
pub async fn set_draft_message(&self, chat_id: ChatId, text: String) -> Result<(), String> {
|
||
if text.is_empty() {
|
||
self.drafts.lock().unwrap().remove(&chat_id.as_i64());
|
||
} else {
|
||
self.drafts.lock().unwrap().insert(chat_id.as_i64(), text.clone());
|
||
}
|
||
|
||
self.send_update(TdUpdate::ChatDraftMessage {
|
||
chat_id,
|
||
draft_text: if text.is_empty() { None } else { Some(text) },
|
||
});
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Отправить действие в чате (typing, etc.)
|
||
pub async fn send_chat_action(&self, chat_id: ChatId, action: String) {
|
||
self.chat_actions.lock().unwrap().push((chat_id.as_i64(), action.clone()));
|
||
|
||
if action == "Typing" {
|
||
*self.typing_chat_id.lock().unwrap() = Some(chat_id.as_i64());
|
||
} else if action == "Cancel" {
|
||
*self.typing_chat_id.lock().unwrap() = None;
|
||
}
|
||
}
|
||
|
||
/// Получить доступные реакции для сообщения
|
||
pub async fn get_message_available_reactions(
|
||
&self,
|
||
_chat_id: ChatId,
|
||
_message_id: MessageId,
|
||
) -> Result<Vec<String>, String> {
|
||
if self.should_fail() {
|
||
return Err("Failed to get available reactions".to_string());
|
||
}
|
||
|
||
Ok(self.available_reactions.lock().unwrap().clone())
|
||
}
|
||
|
||
/// Установить/удалить реакцию
|
||
pub async fn toggle_reaction(
|
||
&self,
|
||
chat_id: ChatId,
|
||
message_id: MessageId,
|
||
emoji: String,
|
||
) -> Result<(), String> {
|
||
if self.should_fail() {
|
||
return Err("Failed to toggle reaction".to_string());
|
||
}
|
||
|
||
// Обновляем реакции на сообщении
|
||
let mut messages = self.messages.lock().unwrap();
|
||
if let Some(chat_msgs) = messages.get_mut(&chat_id.as_i64()) {
|
||
if let Some(msg) = chat_msgs.iter_mut().find(|m| m.id() == message_id) {
|
||
let reactions = &mut msg.interactions.reactions;
|
||
|
||
// Toggle logic
|
||
if let Some(pos) = reactions.iter().position(|r| r.emoji == emoji && r.is_chosen) {
|
||
// Удаляем свою реакцию
|
||
reactions.remove(pos);
|
||
} else if let Some(reaction) = reactions.iter_mut().find(|r| r.emoji == emoji) {
|
||
// Добавляем себя к существующей реакции
|
||
reaction.is_chosen = true;
|
||
reaction.count += 1;
|
||
} else {
|
||
// Добавляем новую реакцию
|
||
reactions.push(ReactionInfo {
|
||
emoji: emoji.clone(),
|
||
count: 1,
|
||
is_chosen: true,
|
||
});
|
||
}
|
||
|
||
let updated_reactions = reactions.clone();
|
||
drop(messages);
|
||
|
||
// Отправляем Update
|
||
self.send_update(TdUpdate::MessageInteractionInfo {
|
||
chat_id,
|
||
message_id,
|
||
reactions: updated_reactions,
|
||
});
|
||
}
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Получить информацию о профиле
|
||
pub async fn get_profile_info(&self, chat_id: ChatId) -> Result<ProfileInfo, String> {
|
||
if self.should_fail() {
|
||
return Err("Failed to get profile info".to_string());
|
||
}
|
||
|
||
self.profiles
|
||
.lock()
|
||
.unwrap()
|
||
.get(&chat_id.as_i64())
|
||
.cloned()
|
||
.ok_or_else(|| "Profile not found".to_string())
|
||
}
|
||
|
||
/// Отметить сообщения как просмотренные
|
||
pub async fn view_messages(&self, chat_id: ChatId, message_ids: Vec<MessageId>) {
|
||
self.viewed_messages
|
||
.lock()
|
||
.unwrap()
|
||
.push((chat_id.as_i64(), message_ids.iter().map(|id| id.as_i64()).collect()));
|
||
}
|
||
|
||
/// Загрузить чаты папки
|
||
pub async fn load_folder_chats(&self, _folder_id: i32, _limit: usize) -> Result<(), String> {
|
||
if self.should_fail() {
|
||
return Err("Failed to load folder chats".to_string());
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
// ==================== Helper Methods ====================
|
||
|
||
/// Отправить update в канал (если он установлен)
|
||
fn send_update(&self, update: TdUpdate) {
|
||
if let Some(tx) = self.update_tx.lock().unwrap().as_ref() {
|
||
let _ = tx.send(update);
|
||
}
|
||
}
|
||
|
||
/// Проверить нужно ли симулировать ошибку
|
||
fn should_fail(&self) -> bool {
|
||
let mut fail = self.fail_next_operation.lock().unwrap();
|
||
if *fail {
|
||
*fail = false; // Сбрасываем после первого использования
|
||
true
|
||
} else {
|
||
false
|
||
}
|
||
}
|
||
|
||
/// Симулировать ошибку в следующей операции
|
||
pub fn fail_next(&self) {
|
||
*self.fail_next_operation.lock().unwrap() = true;
|
||
}
|
||
|
||
/// Симулировать входящее сообщение
|
||
pub fn simulate_incoming_message(&self, chat_id: ChatId, text: String, sender_name: &str) {
|
||
let message_id = MessageId::new(9000 + chrono::Utc::now().timestamp());
|
||
|
||
let message = MessageInfo::new(
|
||
message_id,
|
||
sender_name.to_string(),
|
||
false, // is_outgoing
|
||
text,
|
||
vec![],
|
||
chrono::Utc::now().timestamp() as i32,
|
||
0,
|
||
false,
|
||
false,
|
||
false,
|
||
true,
|
||
None,
|
||
None,
|
||
vec![],
|
||
);
|
||
|
||
// Добавляем в историю
|
||
self.messages
|
||
.lock()
|
||
.unwrap()
|
||
.entry(chat_id.as_i64())
|
||
.or_insert_with(Vec::new)
|
||
.push(message.clone());
|
||
|
||
// Отправляем Update
|
||
self.send_update(TdUpdate::NewMessage { chat_id, message });
|
||
}
|
||
|
||
/// Симулировать typing от собеседника
|
||
pub fn simulate_typing(&self, chat_id: ChatId, user_id: UserId) {
|
||
self.send_update(TdUpdate::ChatAction {
|
||
chat_id,
|
||
user_id,
|
||
action: "Typing".to_string(),
|
||
});
|
||
}
|
||
|
||
/// Симулировать изменение состояния сети
|
||
pub fn simulate_network_change(&self, state: NetworkState) {
|
||
*self.network_state.lock().unwrap() = state.clone();
|
||
self.send_update(TdUpdate::ConnectionState { state });
|
||
}
|
||
|
||
/// Симулировать прочтение сообщений
|
||
pub fn simulate_read_outbox(&self, chat_id: ChatId, last_read_message_id: MessageId) {
|
||
self.send_update(TdUpdate::ChatReadOutbox {
|
||
chat_id,
|
||
last_read_outbox_message_id: last_read_message_id,
|
||
});
|
||
}
|
||
|
||
// ==================== Getters for Test Assertions ====================
|
||
|
||
/// Получить все чаты
|
||
pub fn get_chats(&self) -> Vec<ChatInfo> {
|
||
self.chats.lock().unwrap().clone()
|
||
}
|
||
|
||
/// Получить все папки
|
||
pub fn get_folders(&self) -> Vec<FolderInfo> {
|
||
self.folders.lock().unwrap().clone()
|
||
}
|
||
|
||
/// Получить сообщения чата
|
||
pub fn get_messages(&self, chat_id: i64) -> Vec<MessageInfo> {
|
||
self.messages
|
||
.lock()
|
||
.unwrap()
|
||
.get(&chat_id)
|
||
.cloned()
|
||
.unwrap_or_default()
|
||
}
|
||
|
||
/// Получить отправленные сообщения
|
||
pub fn get_sent_messages(&self) -> Vec<SentMessage> {
|
||
self.sent_messages.lock().unwrap().clone()
|
||
}
|
||
|
||
/// Получить отредактированные сообщения
|
||
pub fn get_edited_messages(&self) -> Vec<EditedMessage> {
|
||
self.edited_messages.lock().unwrap().clone()
|
||
}
|
||
|
||
/// Получить удалённые сообщения
|
||
pub fn get_deleted_messages(&self) -> Vec<DeletedMessages> {
|
||
self.deleted_messages.lock().unwrap().clone()
|
||
}
|
||
|
||
/// Получить пересланные сообщения
|
||
pub fn get_forwarded_messages(&self) -> Vec<ForwardedMessages> {
|
||
self.forwarded_messages.lock().unwrap().clone()
|
||
}
|
||
|
||
/// Получить поисковые запросы
|
||
pub fn get_search_queries(&self) -> Vec<SearchQuery> {
|
||
self.searched_queries.lock().unwrap().clone()
|
||
}
|
||
|
||
/// Получить просмотренные сообщения
|
||
pub fn get_viewed_messages(&self) -> Vec<(i64, Vec<i64>)> {
|
||
self.viewed_messages.lock().unwrap().clone()
|
||
}
|
||
|
||
/// Получить действия в чатах
|
||
pub fn get_chat_actions(&self) -> Vec<(i64, String)> {
|
||
self.chat_actions.lock().unwrap().clone()
|
||
}
|
||
|
||
/// Получить текущее состояние сети
|
||
pub fn get_network_state(&self) -> NetworkState {
|
||
self.network_state.lock().unwrap().clone()
|
||
}
|
||
|
||
/// Получить ID текущего открытого чата
|
||
pub fn get_current_chat_id(&self) -> Option<i64> {
|
||
*self.current_chat_id.lock().unwrap()
|
||
}
|
||
|
||
/// Установить update channel для получения событий
|
||
pub fn set_update_channel(&self, tx: mpsc::UnboundedSender<TdUpdate>) {
|
||
*self.update_tx.lock().unwrap() = Some(tx);
|
||
}
|
||
|
||
/// Очистить всю историю действий
|
||
pub fn clear_all_history(&self) {
|
||
self.sent_messages.lock().unwrap().clear();
|
||
self.edited_messages.lock().unwrap().clear();
|
||
self.deleted_messages.lock().unwrap().clear();
|
||
self.forwarded_messages.lock().unwrap().clear();
|
||
self.searched_queries.lock().unwrap().clear();
|
||
self.viewed_messages.lock().unwrap().clear();
|
||
self.chat_actions.lock().unwrap().clear();
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crate::helpers::test_data::create_test_chat;
|
||
use tele_tui::types::ChatId;
|
||
|
||
#[test]
|
||
fn test_fake_client_creation() {
|
||
let client = FakeTdClient::new();
|
||
assert_eq!(client.get_chats().len(), 0);
|
||
assert_eq!(client.folders.lock().unwrap().len(), 1); // Default "All" folder
|
||
}
|
||
|
||
#[test]
|
||
fn test_fake_client_with_chat() {
|
||
let chat = create_test_chat("Mom", 123);
|
||
let client = FakeTdClient::new().with_chat(chat);
|
||
|
||
let chats = client.get_chats();
|
||
assert_eq!(chats.len(), 1);
|
||
assert_eq!(chats[0].title, "Mom");
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_send_message() {
|
||
let client = FakeTdClient::new();
|
||
let chat_id = ChatId::new(123);
|
||
|
||
let result = client.send_message(chat_id, "Hello".to_string(), None, None).await;
|
||
assert!(result.is_ok());
|
||
|
||
let sent = client.get_sent_messages();
|
||
assert_eq!(sent.len(), 1);
|
||
assert_eq!(sent[0].text, "Hello");
|
||
assert_eq!(client.get_messages(123).len(), 1);
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_edit_message() {
|
||
let client = FakeTdClient::new();
|
||
let chat_id = ChatId::new(123);
|
||
|
||
let msg = client.send_message(chat_id, "Hello".to_string(), None, None).await.unwrap();
|
||
let msg_id = msg.id();
|
||
|
||
let _ = client.edit_message(chat_id, msg_id, "Hello World".to_string()).await;
|
||
|
||
let edited = client.get_edited_messages();
|
||
assert_eq!(edited.len(), 1);
|
||
assert_eq!(client.get_messages(123)[0].text(), "Hello World");
|
||
assert!(client.get_messages(123)[0].metadata.edit_date > 0);
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_delete_message() {
|
||
let client = FakeTdClient::new();
|
||
let chat_id = ChatId::new(123);
|
||
|
||
let msg = client.send_message(chat_id, "Hello".to_string(), None, None).await.unwrap();
|
||
let msg_id = msg.id();
|
||
|
||
let _ = client.delete_messages(chat_id, vec![msg_id], false).await;
|
||
|
||
let deleted = client.get_deleted_messages();
|
||
assert_eq!(deleted.len(), 1);
|
||
assert_eq!(client.get_messages(123).len(), 0);
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_update_channel() {
|
||
let (client, mut rx) = FakeTdClient::new().with_update_channel();
|
||
let chat_id = ChatId::new(123);
|
||
|
||
// Отправляем сообщение
|
||
let _ = client.send_message(chat_id, "Test".to_string(), None, None).await;
|
||
|
||
// Проверяем что получили Update
|
||
if let Some(update) = rx.recv().await {
|
||
match update {
|
||
TdUpdate::NewMessage { chat_id: updated_chat, .. } => {
|
||
assert_eq!(updated_chat, chat_id);
|
||
}
|
||
_ => panic!("Expected NewMessage update"),
|
||
}
|
||
} else {
|
||
panic!("No update received");
|
||
}
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_simulate_incoming_message() {
|
||
let (client, mut rx) = FakeTdClient::new().with_update_channel();
|
||
let chat_id = ChatId::new(123);
|
||
|
||
client.simulate_incoming_message(chat_id, "Hello from Bob".to_string(), "Bob");
|
||
|
||
// Проверяем Update
|
||
if let Some(TdUpdate::NewMessage { message, .. }) = rx.recv().await {
|
||
assert_eq!(message.text(), "Hello from Bob");
|
||
assert_eq!(message.sender_name(), "Bob");
|
||
assert!(!message.is_outgoing());
|
||
}
|
||
|
||
// Проверяем что сообщение добавилось
|
||
assert_eq!(client.get_messages(123).len(), 1);
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_fail_next_operation() {
|
||
let client = FakeTdClient::new();
|
||
let chat_id = ChatId::new(123);
|
||
|
||
// Устанавливаем флаг ошибки
|
||
client.fail_next();
|
||
|
||
// Следующая операция должна упасть
|
||
let result = client.send_message(chat_id, "Test".to_string(), None, None).await;
|
||
assert!(result.is_err());
|
||
|
||
// Но следующая должна пройти
|
||
let result2 = client.send_message(chat_id, "Test2".to_string(), None, None).await;
|
||
assert!(result2.is_ok());
|
||
}
|
||
}
|