Files
telegram-tui/tests/config.rs
Mikhail Kilin bea0bcbed0 feat: implement desktop notifications with comprehensive filtering
Implemented Phase 10 (Desktop Notifications) with three stages:
notify-rust integration, smart filtering, and production polish.

Stage 1 - Base Implementation:
- Add NotificationManager module (src/notifications.rs, 350+ lines)
- Integrate notify-rust 4.11 with feature flag "notifications"
- Implement NotificationsConfig in config.toml (enabled, only_mentions, show_preview)
- Add notification_manager field to TdClient
- Create configure_notifications() method for config integration
- Hook into handle_new_message_update() to send notifications
- Send notifications for messages outside current chat
- Format notification body with sender name and message preview

Stage 2 - Smart Filtering:
- Sync muted chats from Telegram (sync_muted_chats method)
- Filter muted chats from notifications automatically
- Add MessageInfo::has_mention() to detect @username mentions
- Implement only_mentions filter (notify only when mentioned)
- Beautify media labels with emojis (📷 📹 🎤 🎨 📎 etc.)
- Support 10+ media types in notification preview

Stage 3 - Production Polish:
- Add graceful error handling (no panics on notification failure)
- Implement comprehensive logging (tracing::debug!/warn!)
- Add timeout_ms configuration (0 = system default)
- Add urgency configuration (low/normal/critical, Linux only)
- Platform-specific #[cfg] for urgency support
- Log all notification skip reasons at debug level

Hotkey Change:
- Move profile view from 'i' to Ctrl+i to avoid conflicts

Technical Details:
- Cross-platform support (macOS, Linux, Windows)
- Feature flag for optional notifications support
- Graceful fallback when notifications unavailable
- LRU-friendly muted chats sync
- Test coverage for all core notification logic
- All 75 tests passing

Files Changed:
- NEW: src/notifications.rs - Complete NotificationManager
- NEW: config.example.toml - Example configuration with notifications
- Modified: Cargo.toml - Add notify-rust 4.11 dependency
- Modified: src/config/mod.rs - Add NotificationsConfig struct
- Modified: src/tdlib/types.rs - Add has_mention() method
- Modified: src/tdlib/client.rs - Add notification integration
- Modified: src/tdlib/update_handlers.rs - Hook notifications
- Modified: src/config/keybindings.rs - Change profile to Ctrl+i
- Modified: tests/* - Add notification config to tests

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-05 01:27:44 +03:00

273 lines
9.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Integration tests for config flow
use tele_tui::config::{Config, ColorsConfig, GeneralConfig, Keybindings, NotificationsConfig};
/// Test: Дефолтные значения конфигурации
#[test]
fn test_config_default_values() {
let config = Config::default();
// Проверяем дефолтный timezone
assert_eq!(config.general.timezone, "+03:00");
// Проверяем дефолтные цвета
assert_eq!(config.colors.incoming_message, "white");
assert_eq!(config.colors.outgoing_message, "green");
assert_eq!(config.colors.selected_message, "yellow");
assert_eq!(config.colors.reaction_chosen, "yellow");
assert_eq!(config.colors.reaction_other, "gray");
}
/// Test: Создание конфига с кастомными значениями
#[test]
fn test_config_custom_values() {
let config = Config {
general: GeneralConfig {
timezone: "+05:00".to_string(),
},
colors: ColorsConfig {
incoming_message: "cyan".to_string(),
outgoing_message: "blue".to_string(),
selected_message: "red".to_string(),
reaction_chosen: "green".to_string(),
reaction_other: "white".to_string(),
},
keybindings: Keybindings::default(),
notifications: NotificationsConfig::default(),
};
assert_eq!(config.general.timezone, "+05:00");
assert_eq!(config.colors.incoming_message, "cyan");
assert_eq!(config.colors.outgoing_message, "blue");
}
/// Test: Парсинг валидных цветов
#[test]
fn test_parse_valid_colors() {
use ratatui::style::Color;
let config = Config::default();
assert_eq!(config.parse_color("red"), Color::Red);
assert_eq!(config.parse_color("green"), Color::Green);
assert_eq!(config.parse_color("blue"), Color::Blue);
assert_eq!(config.parse_color("yellow"), Color::Yellow);
assert_eq!(config.parse_color("cyan"), Color::Cyan);
assert_eq!(config.parse_color("magenta"), Color::Magenta);
assert_eq!(config.parse_color("white"), Color::White);
assert_eq!(config.parse_color("black"), Color::Black);
assert_eq!(config.parse_color("gray"), Color::Gray);
assert_eq!(config.parse_color("grey"), Color::Gray);
}
/// Test: Парсинг light цветов
#[test]
fn test_parse_light_colors() {
use ratatui::style::Color;
let config = Config::default();
assert_eq!(config.parse_color("lightred"), Color::LightRed);
assert_eq!(config.parse_color("lightgreen"), Color::LightGreen);
assert_eq!(config.parse_color("lightblue"), Color::LightBlue);
assert_eq!(config.parse_color("lightyellow"), Color::LightYellow);
assert_eq!(config.parse_color("lightcyan"), Color::LightCyan);
assert_eq!(config.parse_color("lightmagenta"), Color::LightMagenta);
}
/// Test: Парсинг невалидного цвета использует fallback (White)
#[test]
fn test_parse_invalid_color_fallback() {
use ratatui::style::Color;
let config = Config::default();
// Невалидные цвета должны возвращать White
assert_eq!(config.parse_color("invalid_color"), Color::White);
assert_eq!(config.parse_color(""), Color::White);
assert_eq!(config.parse_color("purple"), Color::White); // purple не поддерживается
assert_eq!(config.parse_color("Orange"), Color::White); // orange не поддерживается
}
/// Test: Case-insensitive парсинг цветов
#[test]
fn test_parse_color_case_insensitive() {
use ratatui::style::Color;
let config = Config::default();
assert_eq!(config.parse_color("RED"), Color::Red);
assert_eq!(config.parse_color("Green"), Color::Green);
assert_eq!(config.parse_color("BLUE"), Color::Blue);
assert_eq!(config.parse_color("YeLLoW"), Color::Yellow);
}
/// Test: Сериализация и десериализация TOML
#[test]
fn test_config_toml_serialization() {
let original_config = Config {
general: GeneralConfig {
timezone: "-05:00".to_string(),
},
colors: ColorsConfig {
incoming_message: "cyan".to_string(),
outgoing_message: "blue".to_string(),
selected_message: "red".to_string(),
reaction_chosen: "green".to_string(),
reaction_other: "white".to_string(),
},
keybindings: Keybindings::default(),
notifications: NotificationsConfig::default(),
};
// Сериализуем в TOML
let toml_string = toml::to_string(&original_config).expect("Failed to serialize config");
// Десериализуем обратно
let deserialized: Config = toml::from_str(&toml_string).expect("Failed to deserialize config");
// Проверяем что всё совпадает
assert_eq!(deserialized.general.timezone, "-05:00");
assert_eq!(deserialized.colors.incoming_message, "cyan");
assert_eq!(deserialized.colors.outgoing_message, "blue");
assert_eq!(deserialized.colors.selected_message, "red");
}
/// Test: Парсинг TOML с частичными данными использует дефолты
#[test]
fn test_config_partial_toml_uses_defaults() {
// TOML только с timezone, без colors
let toml_str = r#"
[general]
timezone = "+02:00"
"#;
let config: Config = toml::from_str(toml_str).expect("Failed to parse partial TOML");
// Timezone должен быть из TOML
assert_eq!(config.general.timezone, "+02:00");
// Colors должны быть дефолтными
assert_eq!(config.colors.incoming_message, "white");
assert_eq!(config.colors.outgoing_message, "green");
}
#[cfg(test)]
mod timezone_tests {
use super::*;
/// Test: Различные форматы timezone
#[test]
fn test_timezone_formats() {
let positive = Config {
general: GeneralConfig {
timezone: "+03:00".to_string(),
},
..Default::default()
};
assert_eq!(positive.general.timezone, "+03:00");
let negative = Config {
general: GeneralConfig {
timezone: "-05:00".to_string(),
},
..Default::default()
};
assert_eq!(negative.general.timezone, "-05:00");
let zero = Config {
general: GeneralConfig {
timezone: "+00:00".to_string(),
},
..Default::default()
};
assert_eq!(zero.general.timezone, "+00:00");
}
}
#[cfg(test)]
mod credentials_tests {
use super::*;
use std::env;
/// Test: Загрузка credentials из переменных окружения
#[test]
fn test_load_credentials_from_env() {
// Устанавливаем env переменные для теста
unsafe {
env::set_var("API_ID", "12345");
env::set_var("API_HASH", "test_hash_from_env");
}
// Загружаем credentials
let result = Config::load_credentials();
// Проверяем что загрузилось из env
// Примечание: этот тест может зафейлиться если есть credentials файл,
// так как он имеет приоритет. Для полноценного тестирования нужно
// моковать файловую систему или использовать временные директории.
if result.is_ok() {
let (api_id, api_hash) = result.unwrap();
// Может быть либо из файла, либо из env
assert!(api_id > 0);
assert!(!api_hash.is_empty());
}
// Очищаем env переменные после теста
unsafe {
env::remove_var("API_ID");
env::remove_var("API_HASH");
}
}
/// Test: Проверка формата ошибки когда credentials не найдены
#[test]
fn test_load_credentials_error_message() {
// Проверяем есть ли credentials файл в системе
let has_credentials_file = Config::credentials_path()
.map(|p| p.exists())
.unwrap_or(false);
// Если есть credentials файл, тест не может проверить ошибку
if has_credentials_file {
// Просто проверяем что credentials загружаются
let result = Config::load_credentials();
assert!(result.is_ok(), "Credentials file exists but loading failed");
return;
}
// Временно сохраняем и удаляем env переменные
let original_api_id = env::var("API_ID").ok();
let original_api_hash = env::var("API_HASH").ok();
unsafe {
env::remove_var("API_ID");
env::remove_var("API_HASH");
}
// Пытаемся загрузить credentials без файла и без env
let result = Config::load_credentials();
// Должна быть ошибка
if result.is_ok() {
// Возможно env переменные установлены глобально и не удаляются
// Тест пропускается
eprintln!("Warning: credentials loaded despite removing env vars");
} else {
// Проверяем формат ошибки
let err_msg = result.unwrap_err();
assert!(!err_msg.is_empty(), "Error message should not be empty");
}
// Восстанавливаем env переменные
unsafe {
if let Some(api_id) = original_api_id {
env::set_var("API_ID", api_id);
}
if let Some(api_hash) = original_api_hash {
env::set_var("API_HASH", api_hash);
}
}
}
}