mod support; use application::ConfigService; use domain::{ AlignItems, ConfigRepository, ContainerNode, DataSource, DataSourceConfig, DataSourceType, Direction, DisplayHint, DisplayHintKind, DomainEvent, JustifyContent, KeyMapping, Layout, LayoutChild, LayoutNode, LayoutPreset, Sizing, WidgetConfig, }; use std::time::Duration; use support::{InMemoryConfigRepository, InMemoryEventPublisher}; #[tokio::test] async fn create_widget_persists_and_emits_event() { let repo = InMemoryConfigRepository::new(); let events = InMemoryEventPublisher::new(); let service = ConfigService::new(&repo, &events); let config = WidgetConfig::new( 1, "weather".into(), DisplayHint::new(DisplayHintKind::IconValue), 1, vec![KeyMapping { source_path: "$.temp".into(), target_key: "temperature".into(), }], ); service.create_widget(config).await.unwrap(); let stored = repo.get_widget(1).await.unwrap(); assert!(stored.is_some()); let emitted = events.emitted(); assert_eq!(emitted.len(), 1); assert!(matches!(emitted[0], DomainEvent::WidgetCreated { id: 1 })); } #[tokio::test] async fn create_data_source_rejects_invalid() { let repo = InMemoryConfigRepository::new(); let events = InMemoryEventPublisher::new(); let service = ConfigService::new(&repo, &events); let source = DataSource { id: 1, name: "bad".into(), source_type: DataSourceType::HttpJson, poll_interval: Duration::from_secs(60), config: DataSourceConfig { url: None, headers: vec![], api_key: None, }, }; let result = service.create_data_source(source).await; assert!(result.is_err()); assert!(events.emitted().is_empty()); } #[tokio::test] async fn create_data_source_persists_valid_and_emits_event() { let repo = InMemoryConfigRepository::new(); let events = InMemoryEventPublisher::new(); let service = ConfigService::new(&repo, &events); let source = DataSource { id: 1, name: "weather".into(), source_type: DataSourceType::Weather, poll_interval: Duration::from_secs(300), config: DataSourceConfig { url: Some("https://api.weather.com".into()), headers: vec![], api_key: None, }, }; service.create_data_source(source).await.unwrap(); let stored = repo.get_data_source(1).await.unwrap(); assert!(stored.is_some()); let emitted = events.emitted(); assert_eq!(emitted.len(), 1); assert!(matches!(emitted[0], DomainEvent::DataSourceAdded { id: 1 })); } #[tokio::test] async fn update_layout_persists_and_emits_event() { let repo = InMemoryConfigRepository::new(); let events = InMemoryEventPublisher::new(); let service = ConfigService::new(&repo, &events); let layout = Layout { root: LayoutNode::Container(ContainerNode { direction: Direction::Row, gap: 4, padding: 2, justify_content: JustifyContent::Start, align_items: AlignItems::Stretch, children: vec![ LayoutChild { sizing: Sizing::Flex(1), node: LayoutNode::Leaf(1), }, LayoutChild { sizing: Sizing::Flex(1), node: LayoutNode::Leaf(2), }, ], }), }; service.update_layout(layout.clone()).await.unwrap(); let stored = repo.get_layout().await.unwrap(); assert_eq!(stored, Some(layout)); assert_eq!(events.emitted().len(), 1); assert!(matches!( events.emitted()[0], DomainEvent::LayoutChanged { .. } )); } #[tokio::test] async fn load_preset_replaces_active_layout() { let repo = InMemoryConfigRepository::new(); let events = InMemoryEventPublisher::new(); let service = ConfigService::new(&repo, &events); let preset_layout = Layout { root: LayoutNode::Container(ContainerNode { direction: Direction::Column, gap: 0, padding: 0, justify_content: JustifyContent::Start, align_items: AlignItems::Stretch, children: vec![LayoutChild { sizing: Sizing::Flex(1), node: LayoutNode::Leaf(5), }], }), }; let preset = LayoutPreset { id: 1, name: "vertical".into(), layout: preset_layout.clone(), }; repo.save_preset(&preset).await.unwrap(); service.load_preset(1).await.unwrap(); let stored = repo.get_layout().await.unwrap(); assert_eq!(stored, Some(preset_layout)); let emitted = events.emitted(); assert_eq!(emitted.len(), 2); assert!(matches!( emitted[0], DomainEvent::LayoutPresetLoaded { id: 1 } )); assert!(matches!(emitted[1], DomainEvent::LayoutChanged { .. })); }