@@ -1580,6 +1580,8 @@ async fn test_email(
15801580mod tests {
15811581 use super :: * ;
15821582 use crate :: entity:: alert_status_history;
1583+ use migration:: { Migrator , MigratorTrait } ;
1584+ use sea_orm:: { ActiveModelTrait , ActiveValue :: Set , Database } ;
15831585
15841586 // ── is_private_ip ─────────────────────────────────────────────────────────
15851587
@@ -1729,4 +1731,192 @@ mod tests {
17291731 assert ! ( detail. is_none( ) ) ;
17301732 assert_eq ! ( kind, "info" ) ;
17311733 }
1734+
1735+ // ── build_alert_dtos ──────────────────────────────────────────────────────
1736+
1737+ fn base_alert_model ( id : i32 , email : & str ) -> alert:: Model {
1738+ alert:: Model {
1739+ id,
1740+ email : email. to_string ( ) ,
1741+ server_name : "matrix.example.com" . to_string ( ) ,
1742+ verified : true ,
1743+ magic_token : None ,
1744+ created_at : time:: OffsetDateTime :: UNIX_EPOCH ,
1745+ last_check_at : None ,
1746+ last_failure_at : None ,
1747+ last_success_at : None ,
1748+ last_email_sent_at : None ,
1749+ failure_count : 0 ,
1750+ is_currently_failing : false ,
1751+ last_recovery_at : None ,
1752+ user_id : None ,
1753+ notify_server_name_change : false ,
1754+ notify_version_change : false ,
1755+ notify_tls_cert_change : false ,
1756+ notify_tls_expiry : false ,
1757+ quiet_hours_enabled : false ,
1758+ quiet_hours_from : "22:00" . to_string ( ) ,
1759+ quiet_hours_to : "07:00" . to_string ( ) ,
1760+ }
1761+ }
1762+
1763+ #[ test]
1764+ fn build_alert_dtos_falls_back_to_alert_email ( ) {
1765+ let alert = base_alert_model ( 1 , "owner@example.com" ) ;
1766+ let result = build_alert_dtos ( vec ! [ alert] , & HashMap :: new ( ) , & HashMap :: new ( ) ) ;
1767+ assert_eq ! ( result. len( ) , 1 ) ;
1768+ assert_eq ! ( result[ 0 ] . notify_emails, vec![ "owner@example.com" ] ) ;
1769+ }
1770+
1771+ #[ test]
1772+ fn build_alert_dtos_uses_notification_emails_map ( ) {
1773+ let alert = base_alert_model ( 1 , "owner@example.com" ) ;
1774+ let mut emails: HashMap < i32 , Vec < String > > = HashMap :: new ( ) ;
1775+ emails. insert (
1776+ 1 ,
1777+ vec ! [ "a@example.com" . to_string( ) , "b@example.com" . to_string( ) ] ,
1778+ ) ;
1779+ let result = build_alert_dtos ( vec ! [ alert] , & emails, & HashMap :: new ( ) ) ;
1780+ assert_eq ! (
1781+ result[ 0 ] . notify_emails,
1782+ vec![ "a@example.com" , "b@example.com" ]
1783+ ) ;
1784+ }
1785+
1786+ #[ test]
1787+ fn build_alert_dtos_empty_email_and_no_map_returns_empty ( ) {
1788+ let alert = base_alert_model ( 1 , "" ) ;
1789+ let result = build_alert_dtos ( vec ! [ alert] , & HashMap :: new ( ) , & HashMap :: new ( ) ) ;
1790+ assert ! ( result[ 0 ] . notify_emails. is_empty( ) ) ;
1791+ }
1792+
1793+ #[ test]
1794+ fn build_alert_dtos_empty_map_entry_falls_back_to_alert_email ( ) {
1795+ let alert = base_alert_model ( 1 , "owner@example.com" ) ;
1796+ let mut emails: HashMap < i32 , Vec < String > > = HashMap :: new ( ) ;
1797+ emails. insert ( 1 , vec ! [ ] ) ;
1798+ let result = build_alert_dtos ( vec ! [ alert] , & emails, & HashMap :: new ( ) ) ;
1799+ assert_eq ! ( result[ 0 ] . notify_emails, vec![ "owner@example.com" ] ) ;
1800+ }
1801+
1802+ #[ test]
1803+ fn build_alert_dtos_includes_webhooks ( ) {
1804+ let webhook = WebhookDto {
1805+ id : 99 ,
1806+ url : "https://hook.example.com" . to_string ( ) ,
1807+ hmac_header : "X-Sig" . to_string ( ) ,
1808+ respect_quiet_hours : false ,
1809+ created_at : time:: OffsetDateTime :: UNIX_EPOCH ,
1810+ } ;
1811+ let mut webhooks: HashMap < i32 , Vec < WebhookDto > > = HashMap :: new ( ) ;
1812+ webhooks. insert ( 5 , vec ! [ webhook] ) ;
1813+ let result = build_alert_dtos (
1814+ vec ! [ base_alert_model( 5 , "a@example.com" ) ] ,
1815+ & HashMap :: new ( ) ,
1816+ & webhooks,
1817+ ) ;
1818+ assert_eq ! ( result[ 0 ] . notify_webhooks. len( ) , 1 ) ;
1819+ assert_eq ! ( result[ 0 ] . notify_webhooks[ 0 ] . url, "https://hook.example.com" ) ;
1820+ }
1821+
1822+ // ── load_emails_by_alert / load_webhooks_by_alert ─────────────────────────
1823+
1824+ async fn make_db ( ) -> sea_orm:: DatabaseConnection {
1825+ let db = Database :: connect ( "sqlite::memory:" ) . await . unwrap ( ) ;
1826+ Migrator :: up ( & db, None ) . await . unwrap ( ) ;
1827+ db
1828+ }
1829+
1830+ async fn insert_minimal_alert ( db : & sea_orm:: DatabaseConnection , id : i32 ) {
1831+ use sea_orm:: { ConnectionTrait , DbBackend , Statement } ;
1832+ db. execute ( Statement :: from_string (
1833+ DbBackend :: Sqlite ,
1834+ format ! (
1835+ "INSERT INTO alert (id, email, server_name, verified, failure_count, \
1836+ is_currently_failing, notify_server_name_change, notify_version_change, \
1837+ notify_tls_cert_change, notify_tls_expiry, quiet_hours_enabled, \
1838+ quiet_hours_from, quiet_hours_to, created_at) \
1839+ VALUES ({id}, 'alert{id}@example.com', 'server{id}.example.com', 1, 0, 0, 0, 0, 0, 0, 0, '22:00', '07:00', datetime('now'))"
1840+ ) ,
1841+ ) )
1842+ . await
1843+ . unwrap ( ) ;
1844+ }
1845+
1846+ #[ tokio:: test]
1847+ async fn load_emails_by_alert_empty_ids_returns_empty_map ( ) {
1848+ let db = make_db ( ) . await ;
1849+ let result = load_emails_by_alert ( & db, & [ ] ) . await . unwrap ( ) ;
1850+ assert ! ( result. is_empty( ) ) ;
1851+ }
1852+
1853+ #[ tokio:: test]
1854+ async fn load_emails_by_alert_no_rows_returns_empty_map ( ) {
1855+ let db = make_db ( ) . await ;
1856+ let result = load_emails_by_alert ( & db, & [ 1 , 2 , 3 ] ) . await . unwrap ( ) ;
1857+ assert ! ( result. is_empty( ) ) ;
1858+ }
1859+
1860+ #[ tokio:: test]
1861+ async fn load_emails_by_alert_groups_by_alert_id ( ) {
1862+ let db = make_db ( ) . await ;
1863+ let now = time:: OffsetDateTime :: now_utc ( ) ;
1864+ insert_minimal_alert ( & db, 1 ) . await ;
1865+ insert_minimal_alert ( & db, 2 ) . await ;
1866+
1867+ // Insert two emails for alert 1, one for alert 2
1868+ for ( alert_id, email) in [
1869+ ( 1 , "a@example.com" ) ,
1870+ ( 1 , "b@example.com" ) ,
1871+ ( 2 , "c@example.com" ) ,
1872+ ] {
1873+ crate :: entity:: alert_notification_email:: ActiveModel {
1874+ id : sea_orm:: ActiveValue :: NotSet ,
1875+ alert_id : Set ( alert_id) ,
1876+ email : Set ( email. to_string ( ) ) ,
1877+ created_at : Set ( now) ,
1878+ }
1879+ . insert ( & db)
1880+ . await
1881+ . unwrap ( ) ;
1882+ }
1883+
1884+ let result = load_emails_by_alert ( & db, & [ 1 , 2 ] ) . await . unwrap ( ) ;
1885+ let mut emails_1 = result[ & 1 ] . clone ( ) ;
1886+ emails_1. sort ( ) ;
1887+ assert_eq ! ( emails_1, vec![ "a@example.com" , "b@example.com" ] ) ;
1888+ assert_eq ! ( result[ & 2 ] , vec![ "c@example.com" ] ) ;
1889+ }
1890+
1891+ #[ tokio:: test]
1892+ async fn load_webhooks_by_alert_empty_ids_returns_empty_map ( ) {
1893+ let db = make_db ( ) . await ;
1894+ let result = load_webhooks_by_alert ( & db, & [ ] ) . await . unwrap ( ) ;
1895+ assert ! ( result. is_empty( ) ) ;
1896+ }
1897+
1898+ #[ tokio:: test]
1899+ async fn load_webhooks_by_alert_groups_webhooks ( ) {
1900+ let db = make_db ( ) . await ;
1901+ let now = time:: OffsetDateTime :: now_utc ( ) ;
1902+ insert_minimal_alert ( & db, 7 ) . await ;
1903+
1904+ crate :: entity:: alert_notification_webhook:: ActiveModel {
1905+ id : sea_orm:: ActiveValue :: NotSet ,
1906+ alert_id : Set ( 7 ) ,
1907+ url : Set ( "https://hook.example.com/a" . to_string ( ) ) ,
1908+ hmac_secret : Set ( None ) ,
1909+ hmac_header : Set ( "X-Sig" . to_string ( ) ) ,
1910+ respect_quiet_hours : Set ( false ) ,
1911+ created_at : Set ( now) ,
1912+ }
1913+ . insert ( & db)
1914+ . await
1915+ . unwrap ( ) ;
1916+
1917+ let result = load_webhooks_by_alert ( & db, & [ 7 ] ) . await . unwrap ( ) ;
1918+ assert_eq ! ( result[ & 7 ] . len( ) , 1 ) ;
1919+ assert_eq ! ( result[ & 7 ] [ 0 ] . url, "https://hook.example.com/a" ) ;
1920+ assert_eq ! ( result[ & 7 ] [ 0 ] . hmac_header, "X-Sig" ) ;
1921+ }
17321922}
0 commit comments