refactor(notification): invert telegram dependency via factory port

NotificationService used to import `crate::adapter::telegram::TelegramAdapter`
directly to construct the adapter inside `test_telegram` — a hexagonal-
architecture violation (core depending on a concrete adapter type).

Introduce a new `AlertNotifierFactory` port. The adapter layer provides
`TelegramAdapterFactory` which implements it and instantiates a fresh
`TelegramAdapter` on each `create()` call (so the test path observes
the most-recently-saved DB config — the original ad-hoc construction
intent is preserved). NotificationService now depends only on the port.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
DaLaw2 2026-04-19 14:24:16 +08:00
parent 1d935bf2da
commit ab35992b87
4 changed files with 68 additions and 11 deletions

View File

@ -8,7 +8,7 @@ use reqwest::Client;
use tokio::time::sleep;
use crate::interface::port::app_repo::AppRepo;
use crate::interface::port::notification::{AlertNotifier, AlertPayload};
use crate::interface::port::notification::{AlertNotifier, AlertNotifierFactory, AlertPayload};
use crate::interface::port::secret_store::SecretStorePort;
use crate::interface::port::setting::SettingRepo;
use crate::model::config::constants::TELEGRAM_MAX_RETRIES;
@ -262,3 +262,26 @@ impl AlertNotifier for TelegramAdapter {
.await
}
}
/// Adapter-side factory that satisfies the `AlertNotifierFactory` port. Holds
/// the same shared dependencies the long-lived adapter uses; each `create()`
/// call instantiates a fresh `TelegramAdapter` so the test path observes
/// whatever config the user just saved.
pub struct TelegramAdapterFactory {
notif: Arc<dyn SettingRepo>,
repo: Arc<dyn AppRepo>,
secrets: Option<Arc<dyn SecretStorePort>>,
}
impl TelegramAdapterFactory {
pub fn new(notif: Arc<dyn SettingRepo>, repo: Arc<dyn AppRepo>, secrets: Option<Arc<dyn SecretStorePort>>) -> Self {
Self { notif, repo, secrets }
}
}
impl AlertNotifierFactory for TelegramAdapterFactory {
fn create(&self) -> Result<Arc<dyn AlertNotifier>, Error> {
let adapter = TelegramAdapter::new(self.notif.clone(), self.repo.clone(), self.secrets.clone())?;
Ok(Arc::new(adapter))
}
}

View File

@ -2,10 +2,9 @@ use std::sync::Arc;
use serde_json::Value;
use crate::adapter::telegram::TelegramAdapter;
use crate::core::email::scheduler::SmtpClient;
use crate::interface::port::app_repo::AppRepo;
use crate::interface::port::notification::AlertNotifier;
use crate::interface::port::notification::AlertNotifierFactory;
use crate::interface::port::secret_store::SecretStorePort;
use crate::interface::port::setting::SettingRepo;
use crate::model::error::Error;
@ -17,11 +16,22 @@ pub struct NotificationService {
notif: Arc<dyn SettingRepo>,
repo: Arc<dyn AppRepo>,
secrets: Arc<dyn SecretStorePort>,
alert_notifier_factory: Arc<dyn AlertNotifierFactory>,
}
impl NotificationService {
pub fn new(notif: Arc<dyn SettingRepo>, repo: Arc<dyn AppRepo>, secrets: Arc<dyn SecretStorePort>) -> Self {
Self { notif, repo, secrets }
pub fn new(
notif: Arc<dyn SettingRepo>,
repo: Arc<dyn AppRepo>,
secrets: Arc<dyn SecretStorePort>,
alert_notifier_factory: Arc<dyn AlertNotifierFactory>,
) -> Self {
Self {
notif,
repo,
secrets,
alert_notifier_factory,
}
}
/// Get Telegram config with redacted bot_token.
@ -65,10 +75,13 @@ impl NotificationService {
self.notif.set_notification_config("telegram", &config_json)
}
/// Send a test Telegram message using current config.
/// Send a test Telegram message using current config. The factory
/// constructs a fresh notifier on every call so the test reflects the
/// most-recently-saved config (the user typically clicks "test"
/// immediately after `set_telegram_config`).
pub async fn test_telegram(&self) -> Result<(), Error> {
let adapter = TelegramAdapter::new(self.notif.clone(), self.repo.clone(), Some(self.secrets.clone()))?;
adapter.send_test_message().await
let notifier = self.alert_notifier_factory.create()?;
notifier.send_test_message().await
}
/// Send a test email using current SMTP config.

View File

@ -18,7 +18,7 @@ use crate::core::auth::jwt::JwtService;
use crate::adapter::access_control_adapter::EbpfAccessControlAdapter;
use crate::adapter::ebpf::EbpfServices;
use crate::adapter::persistence::Database;
use crate::adapter::telegram::TelegramAdapter;
use crate::adapter::telegram::{TelegramAdapter, TelegramAdapterFactory};
use crate::core::acl_service::AclService;
use crate::core::config_service::ConfigService;
use crate::core::dns_filter_service::DnsFilterService;
@ -45,7 +45,7 @@ use crate::interface::port::access_control_admin::AccessControlAdminPort;
use crate::interface::port::app_repo::AppRepo;
use crate::interface::port::dns_filter_api::DnsFilterPort;
use crate::interface::port::geo_block_api::GeoBlockPort;
use crate::interface::port::notification::AlertNotifier;
use crate::interface::port::notification::{AlertNotifier, AlertNotifierFactory};
use crate::interface::port::rate_limit_api::RateLimitPort;
use crate::interface::port::secret_store::SecretStorePort;
use crate::interface::port::setting::SettingRepo;
@ -300,10 +300,16 @@ impl ServiceFactory {
));
let config_service =
Arc::new(ConfigService::new(db.clone() as Arc<dyn AppRepo>).with_secret_store(secret_store_port.clone()));
let notifier_factory: Arc<dyn AlertNotifierFactory> = Arc::new(TelegramAdapterFactory::new(
db.clone() as Arc<dyn SettingRepo>,
db.clone() as Arc<dyn AppRepo>,
Some(secret_store_port.clone()),
));
let notification_service = Arc::new(NotificationService::new(
db.clone() as Arc<dyn SettingRepo>,
db.clone() as Arc<dyn AppRepo>,
secret_store_port,
notifier_factory,
));
let suricata_manager = SuricataManager::new(app_config.clone());

View File

@ -1,6 +1,9 @@
use crate::model::error::Error;
use std::sync::Arc;
use async_trait::async_trait;
use crate::model::error::Error;
/// Alert notification data sent by SOAR engine.
#[derive(Debug, Clone)]
pub struct AlertPayload {
@ -20,3 +23,15 @@ pub trait AlertNotifier: Send + Sync {
async fn send_alert(&self, payload: &AlertPayload) -> Result<(), Error>;
async fn send_test_message(&self) -> Result<(), Error>;
}
/// Port for constructing an `AlertNotifier` on demand.
///
/// `NotificationService::test_telegram` needs to test the *current* DB config,
/// which can have been set after the service was wired. A pre-constructed
/// notifier wouldn't reflect the new credentials, and `core/` cannot reach
/// into `adapter/telegram` to build one. The factory inverts this: the
/// implementation lives in the adapter layer, the core service depends only
/// on this trait, and each call gets a fresh notifier reading the latest config.
pub trait AlertNotifierFactory: Send + Sync {
fn create(&self) -> Result<Arc<dyn AlertNotifier>, Error>;
}