1use std::sync::atomic::{AtomicBool, Ordering};
13use std::time::Duration;
14
15use async_trait::async_trait;
16use chrono::Utc;
17use tokio::time;
18use tracing::debug;
19
20use camel_api::component_metadata::{
21 ComponentCapabilities, ComponentMetadata, OptionKind, UriOption,
22};
23use camel_component_api::UriConfig;
24use camel_component_api::{BoxProcessor, CamelError, Exchange, Message};
25use camel_component_api::{Component, Consumer, ConsumerContext, Endpoint, ProducerContext};
26
27#[derive(Debug, Clone, UriConfig)]
35#[uri_scheme = "timer"]
36#[uri_config(skip_impl, crate = "camel_component_api")]
37pub struct TimerConfig {
38 pub name: String,
40
41 #[allow(dead_code)] #[uri_param(name = "period", default = "1000")]
44 period_ms: u64,
45
46 pub period: Duration,
48
49 #[allow(dead_code)] #[uri_param(name = "delay", default = "0")]
52 delay_ms: u64,
53
54 pub delay: Duration,
56
57 #[uri_param(name = "repeatCount")]
59 pub repeat_count: Option<u32>,
60
61 #[uri_param(name = "fixedRate", default = "false")]
64 pub fixed_rate: bool,
65
66 #[uri_param(name = "includeMetadata", default = "true")]
70 pub include_metadata: bool,
71}
72
73impl TimerConfig {
75 pub fn validate(&self) -> Result<(), CamelError> {
77 if self.name.trim().is_empty() {
78 return Err(CamelError::InvalidUri(
79 "timer name must not be empty".to_string(),
80 ));
81 }
82 if self.period.is_zero() {
83 return Err(CamelError::InvalidUri(
84 "timer period must be greater than 0".to_string(),
85 ));
86 }
87 Ok(())
88 }
89}
90
91impl UriConfig for TimerConfig {
92 fn scheme() -> &'static str {
93 "timer"
94 }
95
96 fn from_uri(uri: &str) -> Result<Self, CamelError> {
97 let parts = camel_component_api::parse_uri(uri)?;
98 Self::from_components(parts)
99 }
100
101 fn from_components(parts: camel_component_api::UriComponents) -> Result<Self, CamelError> {
102 let config = Self::parse_uri_components(parts)?;
103 TimerConfig::validate(&config)?;
104 Ok(config)
105 }
106
107 fn validate(self) -> Result<Self, CamelError> {
108 TimerConfig::validate(&self)?;
110 Ok(self)
111 }
112}
113
114pub struct TimerComponent;
120
121impl TimerComponent {
122 pub fn new() -> Self {
123 Self
124 }
125}
126
127impl Default for TimerComponent {
128 fn default() -> Self {
129 Self::new()
130 }
131}
132
133impl Component for TimerComponent {
134 fn scheme(&self) -> &str {
135 "timer"
136 }
137
138 fn metadata(&self) -> ComponentMetadata {
139 ComponentMetadata {
140 scheme: "timer".to_string(),
141 version: env!("CARGO_PKG_VERSION").to_string(),
142 description: "Generates timer events at fixed intervals".to_string(),
143 uri_syntax: "timer:name?period=1000&delay=0&repeatCount=0".to_string(),
144 capabilities: ComponentCapabilities {
145 supports_consumer: true,
146 ..Default::default()
147 },
148 uri_options: vec![
149 UriOption::new(
150 "period",
151 "Interval between ticks in milliseconds",
152 OptionKind::Int,
153 )
154 .with_default("1000"),
155 UriOption::new(
156 "delay",
157 "Initial delay before first tick in milliseconds",
158 OptionKind::Int,
159 )
160 .with_default("0"),
161 UriOption::new(
162 "repeatCount",
163 "Maximum number of ticks; omit for infinite. 0 = never fires.",
164 OptionKind::Int,
165 ),
166 UriOption::new(
167 "fixedRate",
168 "true=skip missed ticks, false=fire all missed",
169 OptionKind::Bool,
170 )
171 .with_default("false"),
172 UriOption::new(
173 "includeMetadata",
174 "Include CamelTimer* headers in exchanges",
175 OptionKind::Bool,
176 )
177 .with_default("true"),
178 ],
179 ..ComponentMetadata::minimal("timer")
180 }
181 }
182
183 fn create_endpoint(
184 &self,
185 uri: &str,
186 _ctx: &dyn camel_component_api::ComponentContext,
187 ) -> Result<Box<dyn Endpoint>, CamelError> {
188 let config = TimerConfig::from_uri(uri)?;
189 Ok(Box::new(TimerEndpoint {
190 uri: uri.to_string(),
191 config,
192 }))
193 }
194}
195
196pub struct TimerEndpoint {
201 uri: String,
202 config: TimerConfig,
203}
204
205impl Endpoint for TimerEndpoint {
206 fn uri(&self) -> &str {
207 &self.uri
208 }
209
210 fn create_consumer(
211 &self,
212 _rt: std::sync::Arc<dyn camel_component_api::RuntimeObservability>,
213 ) -> Result<Box<dyn Consumer>, CamelError> {
214 Ok(Box::new(TimerConsumer {
215 config: self.config.clone(),
216 started: AtomicBool::new(false),
217 }))
218 }
219
220 fn create_producer(
221 &self,
222 _rt: std::sync::Arc<dyn camel_component_api::RuntimeObservability>,
223 _ctx: &ProducerContext,
224 ) -> Result<BoxProcessor, CamelError> {
225 Err(CamelError::EndpointCreationFailed(
226 "timer endpoint does not support producers".to_string(),
227 ))
228 }
229}
230
231pub struct TimerConsumer {
236 config: TimerConfig,
237 started: AtomicBool,
239}
240
241#[async_trait]
242impl Consumer for TimerConsumer {
243 async fn start(&mut self, context: ConsumerContext) -> Result<(), CamelError> {
244 self.started
246 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
247 .map_err(|_| {
248 CamelError::EndpointCreationFailed("timer consumer already started".to_string())
249 })?;
250
251 TimerConfig::validate(&self.config)?;
252 let config = self.config.clone();
253 let cancel_token = context.cancel_token();
254
255 if !config.delay.is_zero() {
257 tokio::select! {
258 _ = time::sleep(config.delay) => {}
259 _ = cancel_token.cancelled() => {
260 debug!(timer = config.name, "Timer cancelled during initial delay");
261 self.started.store(false, Ordering::SeqCst);
262 return Ok(());
263 }
264 }
265 }
266
267 if config.repeat_count == Some(0) {
269 debug!(timer = config.name, "repeat_count=0, timer will not fire");
270 self.started.store(false, Ordering::SeqCst);
271 return Ok(());
272 }
273
274 let mut interval = time::interval(config.period);
275
276 if config.fixed_rate {
278 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
279 } else {
280 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Burst);
281 }
282
283 let mut count: u32 = 0;
284
285 loop {
286 tokio::select! {
287 _ = cancel_token.cancelled() => {
288 debug!(timer = config.name, "Timer received cancellation, stopping");
289 break;
290 }
291 _ = interval.tick() => {
292 count += 1;
293
294 debug!(timer = config.name, count, "Timer tick");
295
296 let mut exchange = Exchange::new(Message::new(format!(
297 "timer://{} tick #{}",
298 config.name, count
299 )));
300
301 if config.include_metadata {
303 exchange.input.set_header(
304 "CamelTimerName",
305 serde_json::Value::String(config.name.clone()),
306 );
307 exchange
308 .input
309 .set_header("CamelTimerCounter", serde_json::Value::Number(count.into()));
310
311 let now = Utc::now();
313 exchange.input.set_header(
314 "CamelTimerFiredTime",
315 serde_json::Value::String(now.to_rfc3339()),
316 );
317 exchange.input.set_header(
318 "CamelMessageTimestamp",
319 serde_json::Value::Number(
320 now.timestamp_millis().into(),
321 ),
322 );
323 }
324
325 if context.send(exchange).await.is_err() {
326 break;
328 }
329
330 if let Some(max) = config.repeat_count
331 && count >= max
332 {
333 break;
334 }
335 }
336 }
337 }
338
339 self.started.store(false, Ordering::SeqCst);
341 Ok(())
342 }
343
344 async fn stop(&mut self) -> Result<(), CamelError> {
345 self.started.store(false, Ordering::SeqCst);
346 debug!(timer = self.config.name, "timer consumer stopped");
347 Ok(())
348 }
349}
350
351impl TimerConsumer {
352 #[cfg(test)]
354 pub(crate) fn mark_started_for_test(&self) {
355 self.started.store(true, Ordering::SeqCst);
356 }
357}
358
359#[cfg(test)]
364mod tests {
365 use camel_component_api::test_support::PanicRuntimeObservability;
366 fn rt() -> std::sync::Arc<dyn camel_component_api::RuntimeObservability> {
367 std::sync::Arc::new(PanicRuntimeObservability)
368 }
369
370 use super::*;
371 use camel_component_api::NoOpComponentContext;
372
373 #[test]
374 fn test_zero_period_rejected() {
375 let result = TimerConfig::from_uri("timer:tick?period=0");
376 assert!(result.is_err(), "period=0 should be rejected");
377 let err_msg = result.unwrap_err().to_string();
378 assert!(err_msg.contains("period"), "error should mention 'period'");
379 }
380
381 #[test]
382 fn test_timer_empty_name_rejected() {
383 let result = TimerConfig::from_uri("timer:");
384 assert!(result.is_err());
385 let err = result.unwrap_err().to_string();
386 assert!(err.contains("must not be empty"), "unexpected error: {err}");
387 }
388
389 #[test]
390 fn test_timer_config_defaults() {
391 let config = TimerConfig::from_uri("timer:tick").unwrap();
392 assert_eq!(config.name, "tick");
393 assert_eq!(config.period, Duration::from_millis(1000));
394 assert_eq!(config.delay, Duration::from_millis(0));
395 assert_eq!(config.repeat_count, None);
396 }
397
398 #[test]
399 fn test_timer_config_with_params() {
400 let config =
401 TimerConfig::from_uri("timer:myTimer?period=500&delay=100&repeatCount=5").unwrap();
402 assert_eq!(config.name, "myTimer");
403 assert_eq!(config.period, Duration::from_millis(500));
404 assert_eq!(config.delay, Duration::from_millis(100));
405 assert_eq!(config.repeat_count, Some(5));
406 }
407
408 #[test]
409 fn test_timer_config_wrong_scheme() {
410 let result = TimerConfig::from_uri("log:info");
411 assert!(result.is_err());
412 }
413
414 #[test]
415 fn test_timer_component_scheme() {
416 let component = TimerComponent::new();
417 assert_eq!(component.scheme(), "timer");
418 }
419
420 #[test]
421 fn test_timer_component_creates_endpoint() {
422 let component = TimerComponent::new();
423 let endpoint = component.create_endpoint("timer:tick?period=1000", &NoOpComponentContext);
424 assert!(endpoint.is_ok());
425 }
426
427 #[test]
428 fn test_timer_endpoint_no_producer() {
429 let ctx = ProducerContext::new();
430 let component = TimerComponent::new();
431 let endpoint = component
432 .create_endpoint("timer:tick", &NoOpComponentContext)
433 .unwrap();
434 let producer = endpoint.create_producer(rt(), &ctx);
435 assert!(producer.is_err());
436 }
437
438 #[test]
439 fn test_rejects_empty_timer_name() {
440 let mut cfg = TimerConfig::from_uri("timer:tick").unwrap();
441 cfg.name = "".into();
442 assert!(cfg.validate().is_err());
443 }
444
445 #[test]
446 fn test_rejects_zero_period() {
447 let mut cfg = TimerConfig::from_uri("timer:tick").unwrap();
448 cfg.period = Duration::ZERO;
449 assert!(cfg.validate().is_err());
450 }
451
452 #[test]
453 fn test_valid_config_passes() {
454 let mut cfg = TimerConfig::from_uri("timer:tick").unwrap();
455 cfg.name = "myTimer".into();
456 cfg.period = Duration::from_millis(1000);
457 assert!(cfg.validate().is_ok());
458 }
459
460 #[tokio::test]
461 async fn test_repeat_count_zero_fires_never() {
462 let component = TimerComponent::new();
463 let endpoint = component
464 .create_endpoint(
465 "timer:zero-test?period=50&repeatCount=0",
466 &NoOpComponentContext,
467 )
468 .unwrap();
469 let mut consumer = endpoint.create_consumer(rt()).unwrap();
470
471 let (tx, mut rx) = tokio::sync::mpsc::channel(16);
472 let ctx = ConsumerContext::new(
473 tx,
474 tokio_util::sync::CancellationToken::new(),
475 "timer-test-route".to_string(),
476 );
477
478 consumer.start(ctx).await.unwrap();
480
481 tokio::time::sleep(Duration::from_millis(200)).await;
483
484 let mut count = 0;
486 while rx.try_recv().is_ok() {
487 count += 1;
488 }
489 assert_eq!(
490 count, 0,
491 "repeat_count=0 should produce zero fires, got {count}"
492 );
493
494 consumer.stop().await.unwrap();
496 }
497
498 #[tokio::test]
499 async fn test_repeat_count_omitted_fires_indefinitely() {
500 use tokio_util::sync::CancellationToken;
501
502 let token = CancellationToken::new();
503 let (tx, mut rx) = tokio::sync::mpsc::channel(32);
504 let ctx = ConsumerContext::new(tx, token.clone(), "timer-infinite-route".to_string());
505
506 let mut consumer = TimerConsumer {
508 config: TimerConfig::from_uri("timer:infinite-test?period=20").unwrap(),
509 started: AtomicBool::new(false),
510 };
511
512 let handle = tokio::spawn(async move {
513 consumer.start(ctx).await.unwrap();
514 });
515
516 tokio::time::sleep(Duration::from_millis(200)).await;
519 token.cancel();
520 let _ = tokio::time::timeout(Duration::from_secs(1), handle).await;
521
522 let mut count = 0;
523 while rx.try_recv().is_ok() {
524 count += 1;
525 }
526 assert!(
527 count >= 6,
528 "Omitted repeatCount should fire indefinitely; expected >= 6 fires in window, got {count}"
529 );
530 }
531
532 #[tokio::test]
533 async fn test_timer_consumer_fires() {
534 let component = TimerComponent::new();
535 let endpoint = component
536 .create_endpoint("timer:test?period=50&repeatCount=3", &NoOpComponentContext)
537 .unwrap();
538 let mut consumer = endpoint.create_consumer(rt()).unwrap();
539
540 let (tx, mut rx) = tokio::sync::mpsc::channel(16);
541 let ctx = ConsumerContext::new(
542 tx,
543 tokio_util::sync::CancellationToken::new(),
544 "timer-test-route".to_string(),
545 );
546
547 tokio::spawn(async move {
549 consumer.start(ctx).await.unwrap();
550 });
551
552 let mut received = Vec::new();
554 while let Some(envelope) = rx.recv().await {
555 received.push(envelope.exchange);
556 if received.len() == 3 {
557 break;
558 }
559 }
560
561 assert_eq!(received.len(), 3);
562
563 let first = &received[0];
565 assert_eq!(
566 first.input.header("CamelTimerName"),
567 Some(&serde_json::Value::String("test".into()))
568 );
569 assert_eq!(
570 first.input.header("CamelTimerCounter"),
571 Some(&serde_json::Value::Number(1.into()))
572 );
573 }
574
575 #[tokio::test]
576 async fn test_timer_consumer_respects_cancellation() {
577 use tokio_util::sync::CancellationToken;
578
579 let token = CancellationToken::new();
580 let (tx, mut rx) = tokio::sync::mpsc::channel(16);
581 let ctx = ConsumerContext::new(tx, token.clone(), "timer-test-route".to_string());
582
583 let mut consumer = TimerConsumer {
584 config: TimerConfig::from_uri("timer:cancel-test?period=50").unwrap(),
585 started: AtomicBool::new(false),
586 };
587
588 let handle = tokio::spawn(async move {
589 consumer.start(ctx).await.unwrap();
590 });
591
592 tokio::time::sleep(Duration::from_millis(180)).await;
594 token.cancel();
595
596 let result = tokio::time::timeout(Duration::from_secs(1), handle).await;
597 assert!(
598 result.is_ok(),
599 "Consumer should have stopped after cancellation"
600 );
601
602 let mut count = 0;
603 while rx.try_recv().is_ok() {
604 count += 1;
605 }
606 assert!(
607 count >= 2,
608 "Expected at least 2 exchanges before cancellation, got {count}"
609 );
610 }
611
612 #[tokio::test]
613 async fn test_timer_consumer_stop_shuts_down() {
614 let component = TimerComponent::new();
615 let endpoint = component
616 .create_endpoint("timer:stop-test?period=50", &NoOpComponentContext)
617 .unwrap();
618 let mut consumer = endpoint.create_consumer(rt()).unwrap();
619
620 let (tx, mut rx) = tokio::sync::mpsc::channel(16);
621 let token = tokio_util::sync::CancellationToken::new();
622 let ctx = ConsumerContext::new(tx, token.clone(), "timer-test-route".to_string());
623
624 tokio::spawn(async move {
626 consumer.start(ctx).await.unwrap();
627 });
628
629 tokio::time::sleep(Duration::from_millis(180)).await;
631
632 let mut count = 0;
634 while rx.try_recv().is_ok() {
635 count += 1;
636 }
637 assert!(count >= 2, "Expected at least 2 exchanges, got {count}");
638
639 token.cancel();
641 }
642
643 #[test]
645 fn test_fixed_rate_default_is_false() {
646 let config = TimerConfig::from_uri("timer:tick").unwrap();
647 assert!(!config.fixed_rate, "fixedRate should default to false");
648 }
649
650 #[test]
651 fn test_fixed_rate_parsed_from_uri() {
652 let config = TimerConfig::from_uri("timer:tick?fixedRate=true").unwrap();
653 assert!(
654 config.fixed_rate,
655 "fixedRate should be true when set in URI"
656 );
657 }
658
659 #[tokio::test]
661 async fn test_double_start_returns_error() {
662 let component = TimerComponent::new();
663 let endpoint = component
664 .create_endpoint(
665 "timer:double?period=50&repeatCount=2",
666 &NoOpComponentContext,
667 )
668 .unwrap(); let mut consumer = TimerConsumer {
671 config: TimerConfig {
672 name: "double-test".to_string(),
673 period: Duration::from_millis(100),
674 period_ms: 100,
675 delay: Duration::ZERO,
676 delay_ms: 0,
677 repeat_count: None,
678 fixed_rate: false,
679 include_metadata: true,
680 },
681 started: AtomicBool::new(false),
682 };
683
684 consumer.mark_started_for_test();
686
687 let (tx, _rx) = tokio::sync::mpsc::channel(16);
688 let cancel_token = tokio_util::sync::CancellationToken::new();
689 let ctx = ConsumerContext::new(tx, cancel_token.clone(), "timer-test-route".to_string());
690
691 let result = consumer.start(ctx).await;
693 assert!(result.is_err(), "expected double-start to return Err");
694 let err_str = format!("{:?}", result.unwrap_err());
695 assert!(
696 err_str.contains("already started"),
697 "unexpected error: {err_str}"
698 );
699
700 drop(endpoint); }
702
703 #[tokio::test]
705 async fn test_timer_fired_time_and_message_timestamp_headers() {
706 let component = TimerComponent::new();
707 let endpoint = component
708 .create_endpoint(
709 "timer:headers?period=50&repeatCount=1",
710 &NoOpComponentContext,
711 )
712 .unwrap();
713 let mut consumer = endpoint.create_consumer(rt()).unwrap();
714
715 let (tx, mut rx) = tokio::sync::mpsc::channel(16);
716 let ctx = ConsumerContext::new(
717 tx,
718 tokio_util::sync::CancellationToken::new(),
719 "timer-test-route".to_string(),
720 );
721
722 tokio::spawn(async move {
723 consumer.start(ctx).await.unwrap();
724 });
725
726 let envelope = tokio::time::timeout(Duration::from_secs(2), rx.recv())
727 .await
728 .expect("should receive exchange")
729 .expect("envelope should exist");
730
731 let exchange = envelope.exchange;
732
733 let fired_time = exchange
735 .input
736 .header("CamelTimerFiredTime")
737 .expect("CamelTimerFiredTime header should be present");
738 assert!(
739 fired_time.is_string(),
740 "CamelTimerFiredTime should be a string"
741 );
742 let fired_str = fired_time.as_str().unwrap();
743 assert!(
745 chrono::DateTime::parse_from_rfc3339(fired_str).is_ok(),
746 "CamelTimerFiredTime should be valid RFC 3339: {fired_str}"
747 );
748
749 let msg_ts = exchange
751 .input
752 .header("CamelMessageTimestamp")
753 .expect("CamelMessageTimestamp header should be present");
754 assert!(
755 msg_ts.is_number(),
756 "CamelMessageTimestamp should be a number"
757 );
758 let ts_millis = msg_ts.as_i64().expect("should be i64");
759 assert!(ts_millis > 0, "timestamp should be positive");
760 }
761
762 #[test]
763 fn test_timer_fired_time_header_format() {
764 let now = chrono::Utc::now();
766 let rfc = now.to_rfc3339();
767 assert!(chrono::DateTime::parse_from_rfc3339(&rfc).is_ok());
768 let millis = now.timestamp_millis();
769 assert!(millis > 0);
770 }
771
772 #[test]
774 fn test_include_metadata_default_is_true() {
775 let config = TimerConfig::from_uri("timer:tick").unwrap();
776 assert!(
777 config.include_metadata,
778 "includeMetadata should default to true"
779 );
780 }
781
782 #[test]
783 fn test_include_metadata_false_from_uri() {
784 let config = TimerConfig::from_uri("timer:tick?includeMetadata=false").unwrap();
785 assert!(
786 !config.include_metadata,
787 "includeMetadata should be false when set in URI"
788 );
789 }
790
791 #[tokio::test]
792 async fn test_include_metadata_false_omits_headers() {
793 let component = TimerComponent::new();
794 let endpoint = component
795 .create_endpoint(
796 "timer:minimal?period=50&repeatCount=1&includeMetadata=false",
797 &NoOpComponentContext,
798 )
799 .unwrap();
800 let mut consumer = endpoint.create_consumer(rt()).unwrap();
801
802 let (tx, mut rx) = tokio::sync::mpsc::channel(16);
803 let ctx = ConsumerContext::new(
804 tx,
805 tokio_util::sync::CancellationToken::new(),
806 "timer-test-route".to_string(),
807 );
808
809 tokio::spawn(async move {
810 consumer.start(ctx).await.unwrap();
811 });
812
813 let envelope = tokio::time::timeout(Duration::from_secs(2), rx.recv())
814 .await
815 .expect("should receive exchange")
816 .expect("envelope should exist");
817
818 let exchange = envelope.exchange;
819
820 assert!(
822 exchange.input.header("CamelTimerName").is_none(),
823 "CamelTimerName should not be present when includeMetadata=false"
824 );
825 assert!(
826 exchange.input.header("CamelTimerCounter").is_none(),
827 "CamelTimerCounter should not be present when includeMetadata=false"
828 );
829 assert!(
830 exchange.input.header("CamelTimerFiredTime").is_none(),
831 "CamelTimerFiredTime should not be present when includeMetadata=false"
832 );
833 assert!(
834 exchange.input.header("CamelMessageTimestamp").is_none(),
835 "CamelMessageTimestamp should not be present when includeMetadata=false"
836 );
837 }
838
839 #[tokio::test]
840 async fn test_include_metadata_true_includes_all_headers() {
841 let component = TimerComponent::new();
842 let endpoint = component
843 .create_endpoint(
844 "timer:full?period=50&repeatCount=1&includeMetadata=true",
845 &NoOpComponentContext,
846 )
847 .unwrap();
848 let mut consumer = endpoint.create_consumer(rt()).unwrap();
849
850 let (tx, mut rx) = tokio::sync::mpsc::channel(16);
851 let ctx = ConsumerContext::new(
852 tx,
853 tokio_util::sync::CancellationToken::new(),
854 "timer-test-route".to_string(),
855 );
856
857 tokio::spawn(async move {
858 consumer.start(ctx).await.unwrap();
859 });
860
861 let envelope = tokio::time::timeout(Duration::from_secs(2), rx.recv())
862 .await
863 .expect("should receive exchange")
864 .expect("envelope should exist");
865
866 let exchange = envelope.exchange;
867
868 assert!(exchange.input.header("CamelTimerName").is_some());
869 assert!(exchange.input.header("CamelTimerCounter").is_some());
870 assert!(exchange.input.header("CamelTimerFiredTime").is_some());
871 assert!(exchange.input.header("CamelMessageTimestamp").is_some());
872 }
873
874 #[test]
876 fn test_timer_endpoint_is_pub() {
877 let component = TimerComponent::new();
878 let endpoint = component
879 .create_endpoint("timer:pub-test", &NoOpComponentContext)
880 .unwrap();
881 assert_eq!(endpoint.uri(), "timer:pub-test");
882 }
883}