f7a720204a
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用 - 添加所有子项目的完整源代码 - 保留原始 .git 为 .git.bak 备份
683 lines
22 KiB
Rust
683 lines
22 KiB
Rust
//! Black-box integration tests for `ChannelManager`.
|
|
//!
|
|
//! Uses real SQLite (in-memory) and mock EventBroadcaster + MockPlugin.
|
|
//! Covers test-plan items: PS-1..PS-3, EP-1..EP-5, DP-1..DP-4,
|
|
//! TP-1..TP-5, CS-1..CS-2, WS-2.
|
|
|
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use nomifun_api_types::WebSocketMessage;
|
|
use nomifun_channel::error::ChannelError;
|
|
use nomifun_channel::manager::{ChannelManager, EnableChannelSpec, PluginFactory};
|
|
use nomifun_channel::plugin::{ChannelPlugin, PluginCallbacks};
|
|
use nomifun_channel::types::{
|
|
BotInfo, OutgoingMessageType, PluginConfig, PluginCredentials, PluginStatus, PluginType, UnifiedOutgoingMessage,
|
|
};
|
|
use nomifun_common::decrypt_string;
|
|
use nomifun_db::{IChannelRepository, SqliteChannelRepository, init_database_memory};
|
|
use nomifun_realtime::EventBroadcaster;
|
|
use tokio::sync::mpsc;
|
|
|
|
// ── Test infrastructure ─────────────────────────────────────────────
|
|
|
|
struct MockBroadcaster {
|
|
events: Mutex<Vec<WebSocketMessage<serde_json::Value>>>,
|
|
}
|
|
|
|
impl MockBroadcaster {
|
|
fn new() -> Self {
|
|
Self {
|
|
events: Mutex::new(Vec::new()),
|
|
}
|
|
}
|
|
|
|
fn take_events(&self) -> Vec<WebSocketMessage<serde_json::Value>> {
|
|
let mut guard = self.events.lock().unwrap();
|
|
std::mem::take(&mut *guard)
|
|
}
|
|
}
|
|
|
|
impl EventBroadcaster for MockBroadcaster {
|
|
fn broadcast(&self, event: WebSocketMessage<serde_json::Value>) {
|
|
self.events.lock().unwrap().push(event);
|
|
}
|
|
}
|
|
|
|
/// Mock plugin that tracks lifecycle calls.
|
|
struct MockPlugin {
|
|
status: PluginStatus,
|
|
plugin_type: PluginType,
|
|
bot_info: Option<BotInfo>,
|
|
last_error: Option<String>,
|
|
should_fail_init: bool,
|
|
start_calls: Arc<AtomicUsize>,
|
|
}
|
|
|
|
impl MockPlugin {
|
|
fn new(plugin_type: PluginType) -> Self {
|
|
Self {
|
|
status: PluginStatus::Created,
|
|
plugin_type,
|
|
bot_info: None,
|
|
last_error: None,
|
|
should_fail_init: false,
|
|
start_calls: Arc::new(AtomicUsize::new(0)),
|
|
}
|
|
}
|
|
|
|
fn failing(plugin_type: PluginType) -> Self {
|
|
Self {
|
|
should_fail_init: true,
|
|
..Self::new(plugin_type)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl ChannelPlugin for MockPlugin {
|
|
async fn initialize(&mut self, _config: PluginConfig, _callbacks: PluginCallbacks) -> Result<(), ChannelError> {
|
|
if self.should_fail_init {
|
|
self.status = PluginStatus::Error;
|
|
self.last_error = Some("Mock init failure".into());
|
|
return Err(ChannelError::ConnectionFailed("Mock init failure".into()));
|
|
}
|
|
self.status = PluginStatus::Initializing;
|
|
self.bot_info = Some(BotInfo {
|
|
id: "mock_bot".into(),
|
|
username: Some("mock_bot_user".into()),
|
|
display_name: "Mock Bot".into(),
|
|
});
|
|
self.status = PluginStatus::Ready;
|
|
Ok(())
|
|
}
|
|
|
|
async fn start(&mut self) -> Result<(), ChannelError> {
|
|
self.start_calls.fetch_add(1, Ordering::SeqCst);
|
|
self.status = PluginStatus::Starting;
|
|
self.status = PluginStatus::Running;
|
|
Ok(())
|
|
}
|
|
|
|
async fn stop(&mut self) -> Result<(), ChannelError> {
|
|
self.status = PluginStatus::Stopping;
|
|
self.status = PluginStatus::Stopped;
|
|
Ok(())
|
|
}
|
|
|
|
async fn send_message(&self, _chat_id: &str, _message: UnifiedOutgoingMessage) -> Result<String, ChannelError> {
|
|
Ok("mock_msg_id".into())
|
|
}
|
|
|
|
async fn edit_message(
|
|
&self,
|
|
_chat_id: &str,
|
|
_message_id: &str,
|
|
_message: UnifiedOutgoingMessage,
|
|
) -> Result<(), ChannelError> {
|
|
Ok(())
|
|
}
|
|
|
|
fn active_user_count(&self) -> usize {
|
|
0
|
|
}
|
|
|
|
fn bot_info(&self) -> Option<&BotInfo> {
|
|
self.bot_info.as_ref()
|
|
}
|
|
|
|
fn plugin_type(&self) -> PluginType {
|
|
self.plugin_type
|
|
}
|
|
|
|
fn status(&self) -> PluginStatus {
|
|
self.status
|
|
}
|
|
|
|
fn last_error(&self) -> Option<&str> {
|
|
self.last_error.as_deref()
|
|
}
|
|
}
|
|
|
|
fn test_key() -> [u8; 32] {
|
|
[0x42; 32]
|
|
}
|
|
|
|
async fn setup() -> (ChannelManager, Arc<dyn IChannelRepository>, Arc<MockBroadcaster>) {
|
|
let db = init_database_memory().await.unwrap();
|
|
let repo: Arc<dyn IChannelRepository> = Arc::new(SqliteChannelRepository::new(db.pool().clone()));
|
|
let bc = Arc::new(MockBroadcaster::new());
|
|
let (msg_tx, _msg_rx) = mpsc::channel(16);
|
|
let (confirm_tx, _confirm_rx) = mpsc::channel(16);
|
|
let mgr = ChannelManager::new(repo.clone(), bc.clone(), test_key(), msg_tx, confirm_tx);
|
|
// Keep db alive by leaking — test process exits anyway
|
|
std::mem::forget(db);
|
|
(mgr, repo, bc)
|
|
}
|
|
|
|
fn make_factory() -> PluginFactory {
|
|
Box::new(|pt| Some(Box::new(MockPlugin::new(pt))))
|
|
}
|
|
|
|
fn make_failing_factory() -> PluginFactory {
|
|
Box::new(|pt| Some(Box::new(MockPlugin::failing(pt))))
|
|
}
|
|
|
|
fn make_no_impl_factory() -> PluginFactory {
|
|
Box::new(|_pt| None)
|
|
}
|
|
|
|
fn make_counting_factory() -> (PluginFactory, Arc<AtomicUsize>) {
|
|
let start_calls = Arc::new(AtomicUsize::new(0));
|
|
let captured = Arc::clone(&start_calls);
|
|
let factory = Box::new(move |pt| {
|
|
let mut plugin = MockPlugin::new(pt);
|
|
plugin.start_calls = Arc::clone(&captured);
|
|
Some(Box::new(plugin) as Box<dyn ChannelPlugin>)
|
|
});
|
|
(factory, start_calls)
|
|
}
|
|
|
|
fn make_telegram_config() -> serde_json::Value {
|
|
serde_json::json!({
|
|
"credentials": { "token": "bot:valid123" },
|
|
"config": { "mode": "polling" }
|
|
})
|
|
}
|
|
|
|
fn make_lark_config() -> serde_json::Value {
|
|
serde_json::json!({
|
|
"credentials": {
|
|
"app_id": "cli_abc",
|
|
"app_secret": "secret123"
|
|
}
|
|
})
|
|
}
|
|
|
|
fn make_plugin_config() -> PluginConfig {
|
|
PluginConfig {
|
|
credentials: PluginCredentials {
|
|
token: Some("bot:valid123".into()),
|
|
..Default::default()
|
|
},
|
|
config: None,
|
|
}
|
|
}
|
|
|
|
fn make_test_outgoing() -> UnifiedOutgoingMessage {
|
|
UnifiedOutgoingMessage {
|
|
message_type: OutgoingMessageType::Text,
|
|
text: Some("hello".into()),
|
|
parse_mode: None,
|
|
buttons: None,
|
|
keyboard: None,
|
|
image_url: None,
|
|
file_url: None,
|
|
file_name: None,
|
|
media_actions: None,
|
|
reply_to_message_id: None,
|
|
silent: None,
|
|
}
|
|
}
|
|
|
|
// ── PS-1: Get plugin status (no plugins) ──────────────────────────
|
|
|
|
#[tokio::test]
|
|
async fn ps1_get_status_empty() {
|
|
let (mgr, _repo, _bc) = setup().await;
|
|
let statuses = mgr.get_plugin_status().await.unwrap();
|
|
assert!(statuses.is_empty());
|
|
}
|
|
|
|
// ── PS-2: Get plugin status (with plugins) ────────────────────────
|
|
|
|
#[tokio::test]
|
|
async fn ps2_get_status_with_plugins() {
|
|
let (mgr, _repo, _bc) = setup().await;
|
|
let factory = make_factory();
|
|
|
|
mgr.enable_plugin(&EnableChannelSpec::legacy("telegram"), &make_telegram_config(), &factory)
|
|
.await
|
|
.unwrap();
|
|
|
|
let statuses = mgr.get_plugin_status().await.unwrap();
|
|
assert_eq!(statuses.len(), 1);
|
|
assert_eq!(statuses[0].plugin_id, "telegram");
|
|
assert_eq!(statuses[0].plugin_type, "telegram");
|
|
assert_eq!(statuses[0].name, "Telegram Bot");
|
|
assert!(statuses[0].enabled);
|
|
assert_eq!(statuses[0].status.as_deref(), Some("running"));
|
|
}
|
|
|
|
// ── EP-1: Enable Telegram plugin ──────────────────────────────────
|
|
|
|
#[tokio::test]
|
|
async fn ep1_enable_telegram_plugin() {
|
|
let (mgr, repo, _bc) = setup().await;
|
|
let factory = make_factory();
|
|
|
|
mgr.enable_plugin(&EnableChannelSpec::legacy("telegram"), &make_telegram_config(), &factory)
|
|
.await
|
|
.unwrap();
|
|
|
|
// Plugin persisted in DB
|
|
let row = repo.get_plugin("telegram").await.unwrap().unwrap();
|
|
assert!(row.enabled);
|
|
assert_eq!(row.r#type, "telegram");
|
|
assert_eq!(row.name, "Telegram Bot");
|
|
assert!(row.last_connected.is_some());
|
|
|
|
// Plugin is running in memory
|
|
assert!(mgr.is_plugin_running("telegram"));
|
|
assert_eq!(mgr.active_plugin_count(), 1);
|
|
}
|
|
|
|
// ── EP-2: Re-enable updates config ────────────────────────────────
|
|
|
|
#[tokio::test]
|
|
async fn ep2_re_enable_updates_config() {
|
|
let (mgr, repo, _bc) = setup().await;
|
|
let factory = make_factory();
|
|
|
|
mgr.enable_plugin(&EnableChannelSpec::legacy("telegram"), &make_telegram_config(), &factory)
|
|
.await
|
|
.unwrap();
|
|
|
|
// Re-enable with different config
|
|
let new_config = serde_json::json!({
|
|
"credentials": { "token": "bot:new_token_456" },
|
|
"config": { "mode": "webhook", "webhook_url": "https://example.com" }
|
|
});
|
|
mgr.enable_plugin(&EnableChannelSpec::legacy("telegram"), &new_config, &factory).await.unwrap();
|
|
|
|
// Still only one plugin
|
|
assert_eq!(mgr.active_plugin_count(), 1);
|
|
|
|
// Config should be updated
|
|
let row = repo.get_plugin("telegram").await.unwrap().unwrap();
|
|
let decrypted = decrypt_string(&row.config, &test_key()).unwrap();
|
|
let config: PluginConfig = serde_json::from_str(&decrypted).unwrap();
|
|
assert_eq!(config.credentials.token.as_deref(), Some("bot:new_token_456"));
|
|
}
|
|
|
|
// ── EP-5: Invalid plugin ID ──────────────────────────────────────
|
|
|
|
#[tokio::test]
|
|
async fn ep5_invalid_plugin_id() {
|
|
let (mgr, _repo, _bc) = setup().await;
|
|
let factory = make_factory();
|
|
|
|
let err = mgr
|
|
.enable_plugin(&EnableChannelSpec::legacy("nonexistent"), &make_telegram_config(), &factory)
|
|
.await
|
|
.unwrap_err();
|
|
assert!(matches!(err, ChannelError::InvalidPluginType(_)));
|
|
}
|
|
|
|
// ── EP-3/EP-4: Missing required fields ────────────────────────────
|
|
|
|
#[tokio::test]
|
|
async fn ep3_ep4_invalid_config_structure() {
|
|
let (mgr, _repo, _bc) = setup().await;
|
|
let factory = make_factory();
|
|
|
|
// Missing credentials entirely
|
|
let bad = serde_json::json!({ "wrong_key": "value" });
|
|
let err = mgr.enable_plugin(&EnableChannelSpec::legacy("telegram"), &bad, &factory).await.unwrap_err();
|
|
assert!(matches!(err, ChannelError::InvalidConfig(_)));
|
|
}
|
|
|
|
// ── DP-1: Disable enabled plugin ──────────────────────────────────
|
|
|
|
#[tokio::test]
|
|
async fn dp1_disable_enabled_plugin() {
|
|
let (mgr, repo, _bc) = setup().await;
|
|
let factory = make_factory();
|
|
|
|
mgr.enable_plugin(&EnableChannelSpec::legacy("telegram"), &make_telegram_config(), &factory)
|
|
.await
|
|
.unwrap();
|
|
mgr.disable_plugin("telegram").await.unwrap();
|
|
|
|
assert_eq!(mgr.active_plugin_count(), 0);
|
|
assert!(!mgr.is_plugin_running("telegram"));
|
|
|
|
let row = repo.get_plugin("telegram").await.unwrap().unwrap();
|
|
assert!(!row.enabled);
|
|
assert_eq!(row.status.as_deref(), Some("stopped"));
|
|
}
|
|
|
|
// ── DP-2: Disable already disabled (idempotent) ──────────────────
|
|
|
|
#[tokio::test]
|
|
async fn dp2_disable_already_disabled() {
|
|
let (mgr, _repo, _bc) = setup().await;
|
|
let factory = make_factory();
|
|
|
|
mgr.enable_plugin(&EnableChannelSpec::legacy("telegram"), &make_telegram_config(), &factory)
|
|
.await
|
|
.unwrap();
|
|
mgr.disable_plugin("telegram").await.unwrap();
|
|
|
|
// Second disable should not error
|
|
mgr.disable_plugin("telegram").await.unwrap();
|
|
assert_eq!(mgr.active_plugin_count(), 0);
|
|
}
|
|
|
|
// ── TP-1: Test valid credentials returns bot username ─────────────
|
|
|
|
#[tokio::test]
|
|
async fn tp1_test_valid_credentials() {
|
|
let (mgr, _repo, _bc) = setup().await;
|
|
let factory = make_factory();
|
|
|
|
let result = mgr
|
|
.test_plugin("telegram", make_plugin_config(), &factory)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(result.as_deref(), Some("mock_bot_user"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_plugin_initializes_without_starting_runtime() {
|
|
let (mgr, _repo, _bc) = setup().await;
|
|
let (factory, start_calls) = make_counting_factory();
|
|
|
|
let username = mgr
|
|
.test_plugin("telegram", make_plugin_config(), &factory)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(username.as_deref(), Some("mock_bot_user"));
|
|
assert_eq!(
|
|
start_calls.load(Ordering::SeqCst),
|
|
0,
|
|
"credential tests must not start long-running plugin runtime"
|
|
);
|
|
}
|
|
|
|
// ── TP-2: Test invalid credentials propagates error ───────────────
|
|
|
|
#[tokio::test]
|
|
async fn tp2_test_invalid_credentials() {
|
|
let (mgr, _repo, _bc) = setup().await;
|
|
let factory = make_failing_factory();
|
|
|
|
let err = mgr.test_plugin("telegram", make_plugin_config(), &factory).await;
|
|
assert!(err.is_err());
|
|
}
|
|
|
|
// ── TP-4: Missing plugin ID ──────────────────────────────────────
|
|
|
|
#[tokio::test]
|
|
async fn tp4_test_invalid_plugin_type() {
|
|
let (mgr, _repo, _bc) = setup().await;
|
|
let factory = make_factory();
|
|
|
|
let err = mgr
|
|
.test_plugin("nonexistent", make_plugin_config(), &factory)
|
|
.await
|
|
.unwrap_err();
|
|
assert!(matches!(err, ChannelError::InvalidPluginType(_)));
|
|
}
|
|
|
|
// ── TP: Test does not persist ─────────────────────────────────────
|
|
|
|
#[tokio::test]
|
|
async fn tp_test_does_not_persist() {
|
|
let (mgr, repo, _bc) = setup().await;
|
|
let factory = make_factory();
|
|
|
|
mgr.test_plugin("telegram", make_plugin_config(), &factory)
|
|
.await
|
|
.unwrap();
|
|
|
|
let plugins = repo.get_all_plugins().await.unwrap();
|
|
assert!(plugins.is_empty());
|
|
assert_eq!(mgr.active_plugin_count(), 0);
|
|
}
|
|
|
|
// ── CS-1: Credentials stored encrypted ────────────────────────────
|
|
|
|
#[tokio::test]
|
|
async fn cs1_credentials_stored_encrypted() {
|
|
let (mgr, repo, _bc) = setup().await;
|
|
let factory = make_factory();
|
|
|
|
mgr.enable_plugin(&EnableChannelSpec::legacy("telegram"), &make_telegram_config(), &factory)
|
|
.await
|
|
.unwrap();
|
|
|
|
let row = repo.get_plugin("telegram").await.unwrap().unwrap();
|
|
|
|
// Config should not contain plaintext token
|
|
assert!(!row.config.contains("bot:valid123"));
|
|
assert!(!row.config.contains("token"));
|
|
|
|
// Should be valid base64 (encrypted output)
|
|
assert!(base64_looks_valid(&row.config));
|
|
|
|
// Decryption should yield the original config
|
|
let decrypted = decrypt_string(&row.config, &test_key()).unwrap();
|
|
let config: PluginConfig = serde_json::from_str(&decrypted).unwrap();
|
|
assert_eq!(config.credentials.token.as_deref(), Some("bot:valid123"));
|
|
}
|
|
|
|
// ── CS-2: Status response does not leak credentials ───────────────
|
|
|
|
#[tokio::test]
|
|
async fn cs2_status_does_not_leak_credentials() {
|
|
let (mgr, _repo, _bc) = setup().await;
|
|
let factory = make_factory();
|
|
|
|
mgr.enable_plugin(&EnableChannelSpec::legacy("telegram"), &make_telegram_config(), &factory)
|
|
.await
|
|
.unwrap();
|
|
|
|
let statuses = mgr.get_plugin_status().await.unwrap();
|
|
let json = serde_json::to_string(&statuses).unwrap();
|
|
|
|
// No sensitive fields should appear
|
|
assert!(!json.contains("bot:valid123"));
|
|
assert!(!json.contains("credentials"));
|
|
assert!(!json.contains("config"));
|
|
// But the plugin metadata should be there
|
|
assert!(json.contains("telegram"));
|
|
assert!(json.contains("Telegram Bot"));
|
|
}
|
|
|
|
// ── WS-2: Plugin status change event broadcast ───────────────────
|
|
|
|
#[tokio::test]
|
|
async fn ws2_enable_broadcasts_status_change() {
|
|
let (mgr, _repo, bc) = setup().await;
|
|
let factory = make_factory();
|
|
|
|
mgr.enable_plugin(&EnableChannelSpec::legacy("telegram"), &make_telegram_config(), &factory)
|
|
.await
|
|
.unwrap();
|
|
|
|
let events = bc.take_events();
|
|
let status_events: Vec<_> = events
|
|
.iter()
|
|
.filter(|e| e.name == "channel.plugin-status-changed")
|
|
.collect();
|
|
assert!(!status_events.is_empty());
|
|
assert_eq!(status_events.last().unwrap().data["plugin_id"], "telegram");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn ws2_disable_broadcasts_status_change() {
|
|
let (mgr, _repo, bc) = setup().await;
|
|
let factory = make_factory();
|
|
|
|
mgr.enable_plugin(&EnableChannelSpec::legacy("telegram"), &make_telegram_config(), &factory)
|
|
.await
|
|
.unwrap();
|
|
bc.take_events(); // clear enable events
|
|
|
|
mgr.disable_plugin("telegram").await.unwrap();
|
|
|
|
let events = bc.take_events();
|
|
let status_events: Vec<_> = events
|
|
.iter()
|
|
.filter(|e| e.name == "channel.plugin-status-changed")
|
|
.collect();
|
|
assert!(!status_events.is_empty());
|
|
}
|
|
|
|
// ── Restore: enabled plugins start on restore ────────────────────
|
|
|
|
#[tokio::test]
|
|
async fn restore_starts_enabled_plugins() {
|
|
let (mgr, _repo, _bc) = setup().await;
|
|
let factory = make_factory();
|
|
|
|
// First enable and persist a plugin
|
|
mgr.enable_plugin(&EnableChannelSpec::legacy("telegram"), &make_telegram_config(), &factory)
|
|
.await
|
|
.unwrap();
|
|
|
|
// Simulate shutdown
|
|
mgr.shutdown().await;
|
|
assert_eq!(mgr.active_plugin_count(), 0);
|
|
|
|
// Restore should bring it back
|
|
mgr.restore_plugins(&factory).await.unwrap();
|
|
assert_eq!(mgr.active_plugin_count(), 1);
|
|
assert!(mgr.is_plugin_running("telegram"));
|
|
}
|
|
|
|
// ── Restore: disabled plugins are skipped ─────────────────────────
|
|
|
|
#[tokio::test]
|
|
async fn restore_skips_disabled_plugins() {
|
|
let (mgr, _repo, _bc) = setup().await;
|
|
let factory = make_factory();
|
|
|
|
mgr.enable_plugin(&EnableChannelSpec::legacy("telegram"), &make_telegram_config(), &factory)
|
|
.await
|
|
.unwrap();
|
|
mgr.disable_plugin("telegram").await.unwrap();
|
|
|
|
mgr.restore_plugins(&factory).await.unwrap();
|
|
assert_eq!(mgr.active_plugin_count(), 0);
|
|
}
|
|
|
|
// ── Multiple plugins ──────────────────────────────────────────────
|
|
|
|
#[tokio::test]
|
|
async fn enable_multiple_plugins() {
|
|
let (mgr, _repo, _bc) = setup().await;
|
|
let factory = make_factory();
|
|
|
|
mgr.enable_plugin(&EnableChannelSpec::legacy("telegram"), &make_telegram_config(), &factory)
|
|
.await
|
|
.unwrap();
|
|
mgr.enable_plugin(&EnableChannelSpec::legacy("lark"), &make_lark_config(), &factory).await.unwrap();
|
|
|
|
assert_eq!(mgr.active_plugin_count(), 2);
|
|
assert!(mgr.is_plugin_running("telegram"));
|
|
assert!(mgr.is_plugin_running("lark"));
|
|
|
|
let statuses = mgr.get_plugin_status().await.unwrap();
|
|
assert_eq!(statuses.len(), 2);
|
|
}
|
|
|
|
// ── Shutdown stops all ────────────────────────────────────────────
|
|
|
|
#[tokio::test]
|
|
async fn shutdown_stops_all() {
|
|
let (mgr, _repo, _bc) = setup().await;
|
|
let factory = make_factory();
|
|
|
|
mgr.enable_plugin(&EnableChannelSpec::legacy("telegram"), &make_telegram_config(), &factory)
|
|
.await
|
|
.unwrap();
|
|
mgr.enable_plugin(&EnableChannelSpec::legacy("lark"), &make_lark_config(), &factory).await.unwrap();
|
|
|
|
mgr.shutdown().await;
|
|
assert_eq!(mgr.active_plugin_count(), 0);
|
|
}
|
|
|
|
// ── Send/Edit message routing ─────────────────────────────────────
|
|
|
|
#[tokio::test]
|
|
async fn send_message_routes_to_plugin() {
|
|
let (mgr, _repo, _bc) = setup().await;
|
|
let factory = make_factory();
|
|
|
|
mgr.enable_plugin(&EnableChannelSpec::legacy("telegram"), &make_telegram_config(), &factory)
|
|
.await
|
|
.unwrap();
|
|
|
|
let msg_id = mgr
|
|
.send_message("telegram", "chat_1", make_test_outgoing())
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(msg_id, "mock_msg_id");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn send_message_not_running_fails() {
|
|
let (mgr, _repo, _bc) = setup().await;
|
|
|
|
let err = mgr
|
|
.send_message("telegram", "chat_1", make_test_outgoing())
|
|
.await
|
|
.unwrap_err();
|
|
assert!(matches!(err, ChannelError::PluginNotFound(_)));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn edit_message_routes_to_plugin() {
|
|
let (mgr, _repo, _bc) = setup().await;
|
|
let factory = make_factory();
|
|
|
|
mgr.enable_plugin(&EnableChannelSpec::legacy("telegram"), &make_telegram_config(), &factory)
|
|
.await
|
|
.unwrap();
|
|
|
|
mgr.edit_message("telegram", "chat_1", "msg_1", make_test_outgoing())
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
// ── Init failure sets error in DB ─────────────────────────────────
|
|
|
|
#[tokio::test]
|
|
async fn enable_failure_sets_error_in_db() {
|
|
let (mgr, repo, _bc) = setup().await;
|
|
let factory = make_failing_factory();
|
|
|
|
let err = mgr.enable_plugin(&EnableChannelSpec::legacy("telegram"), &make_telegram_config(), &factory).await;
|
|
assert!(err.is_err());
|
|
|
|
// Plugin should exist in DB with error status
|
|
let row = repo.get_plugin("telegram").await.unwrap().unwrap();
|
|
assert_eq!(row.status.as_deref(), Some("error"));
|
|
assert_eq!(mgr.active_plugin_count(), 0);
|
|
}
|
|
|
|
// ── No implementation factory ─────────────────────────────────────
|
|
|
|
#[tokio::test]
|
|
async fn enable_no_implementation_fails() {
|
|
let (mgr, _repo, _bc) = setup().await;
|
|
let factory = make_no_impl_factory();
|
|
|
|
let err = mgr
|
|
.enable_plugin(&EnableChannelSpec::legacy("telegram"), &make_telegram_config(), &factory)
|
|
.await
|
|
.unwrap_err();
|
|
assert!(matches!(err, ChannelError::InvalidPluginType(_)));
|
|
}
|
|
|
|
// ── Helper ────────────────────────────────────────────────────────
|
|
|
|
fn base64_looks_valid(s: &str) -> bool {
|
|
s.chars()
|
|
.all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '=')
|
|
&& s.len() > 20
|
|
}
|