Skip to main content

aster_forge_audit/
lib.rs

1//! Shared audit runtime integration for Aster services.
2//!
3//! Product crates own audit actions, detail schemas, authorization rules,
4//! operator-facing presentation. Forge owns the runtime lifecycle contract shared
5//! by Aster products: record a best-effort server shutdown audit event, then stop/join
6//! the caller-owned audit writer before database handles close. With the `db-writer` feature,
7//! Forge also owns the shared buffered database writer used by Aster products.
8//! Products that also use a mail outbox can enable the `mail-outbox-dependency`
9//! feature to make the standard audit constructors depend on the mail outbox drain
10//! component.
11#![cfg_attr(
12    not(test),
13    deny(
14        clippy::unwrap_used,
15        clippy::unreachable,
16        clippy::expect_used,
17        clippy::panic,
18        clippy::unimplemented,
19        clippy::todo
20    )
21)]
22
23use std::future::Future;
24
25#[cfg(feature = "db-writer")]
26mod db_writer;
27
28use aster_forge_runtime::{
29    RuntimeComponentBundleRegistration, RuntimeComponentKind, RuntimeComponentRegistry,
30    StartupPhaseFailurePolicy, runtime_component,
31};
32
33#[cfg(feature = "db-writer")]
34pub use db_writer::{
35    AUDIT_WRITER_START_PHASE, AuditLogBufferConfig, AuditWriter, DEFAULT_AUDIT_LOG_BATCH_SIZE,
36    DEFAULT_AUDIT_LOG_DELAYED_FLUSH_AFTER, DEFAULT_AUDIT_LOG_QUEUE_CAPACITY,
37    audit_component_with_writer, audit_component_with_writer_after, audit_writer_component,
38};
39
40/// Stable component name used for process lifecycle audit records.
41pub const AUDIT_LOGS_COMPONENT: &str = "audit_logs";
42/// Stable component name used for the buffered audit manager.
43pub const AUDIT_MANAGER_COMPONENT: &str = "audit_manager";
44/// Stable startup phase name for recording the process start event.
45pub const SERVER_START_AUDIT_PHASE: &str = "server_start_audit";
46/// Stable shutdown phase name for recording the process shutdown event.
47pub const SERVER_SHUTDOWN_AUDIT_PHASE: &str = "server_shutdown_audit";
48/// Stable shutdown phase name for flushing the buffered audit manager.
49pub const AUDIT_MANAGER_FLUSH_SHUTDOWN_PHASE: &str = "audit_manager_flush";
50
51#[cfg(feature = "mail-outbox-dependency")]
52const DEFAULT_SERVER_SHUTDOWN_AUDIT_DEPENDENCIES: &[&str] =
53    &[aster_forge_mail::MAIL_OUTBOX_COMPONENT];
54#[cfg(not(feature = "mail-outbox-dependency"))]
55const DEFAULT_SERVER_SHUTDOWN_AUDIT_DEPENDENCIES: &[&str] = &[];
56
57/// Creates the full audit lifecycle component bundle used by product entrypoints.
58///
59/// `resources` is product-defined. It commonly contains a database connection
60/// and runtime config snapshot. `record_server_start` and `record_server_shutdown`
61/// record the product's own lifecycle audit entries. `flush_audit_manager` drains
62/// the product's buffered audit writer. With the `mail-outbox-dependency`
63/// feature enabled, the shutdown audit phase automatically runs after the mail
64/// outbox drain component.
65pub fn audit_component<T, StartFn, StartFut, ShutdownFn, ShutdownFut, FlushFn, FlushFut>(
66    resources: T,
67    record_server_start: StartFn,
68    record_server_shutdown: ShutdownFn,
69    flush_audit_manager: FlushFn,
70) -> RuntimeComponentBundleRegistration<impl aster_forge_runtime::RuntimeComponentBundle>
71where
72    T: Clone + Send + 'static,
73    StartFn: FnOnce(T) -> StartFut + Send + Sync + 'static,
74    StartFut: Future<Output = Result<(), String>> + Send + 'static,
75    ShutdownFn: FnOnce(T) -> ShutdownFut + Send + Sync + 'static,
76    ShutdownFut: Future<Output = Result<(), String>> + Send + 'static,
77    FlushFn: FnOnce(()) -> FlushFut + Send + Sync + 'static,
78    FlushFut: Future<Output = Result<(), String>> + Send + 'static,
79{
80    audit_component_after(
81        resources,
82        DEFAULT_SERVER_SHUTDOWN_AUDIT_DEPENDENCIES,
83        record_server_start,
84        record_server_shutdown,
85        flush_audit_manager,
86    )
87}
88
89/// Creates the full audit lifecycle component for hooks that cannot fail.
90///
91/// This is the preferred entrypoint when product audit hooks already handle
92/// write failures internally and return `()`. It preserves the same component
93/// graph and feature-provided dependencies as [`audit_component`] without
94/// forcing every product to wrap each hook in `Ok(())`.
95pub fn audit_component_infallible<
96    T,
97    StartFn,
98    StartFut,
99    ShutdownFn,
100    ShutdownFut,
101    FlushFn,
102    FlushFut,
103>(
104    resources: T,
105    record_server_start: StartFn,
106    record_server_shutdown: ShutdownFn,
107    flush_audit_manager: FlushFn,
108) -> RuntimeComponentBundleRegistration<impl aster_forge_runtime::RuntimeComponentBundle>
109where
110    T: Clone + Send + 'static,
111    StartFn: FnOnce(T) -> StartFut + Send + Sync + 'static,
112    StartFut: Future<Output = ()> + Send + 'static,
113    ShutdownFn: FnOnce(T) -> ShutdownFut + Send + Sync + 'static,
114    ShutdownFut: Future<Output = ()> + Send + 'static,
115    FlushFn: FnOnce(()) -> FlushFut + Send + Sync + 'static,
116    FlushFut: Future<Output = ()> + Send + 'static,
117{
118    audit_component_after_infallible(
119        resources,
120        DEFAULT_SERVER_SHUTDOWN_AUDIT_DEPENDENCIES,
121        record_server_start,
122        record_server_shutdown,
123        flush_audit_manager,
124    )
125}
126
127/// Creates the full audit lifecycle component bundle with caller-provided
128/// shutdown dependencies for the server-shutdown audit phase.
129///
130/// Use this when a product has another product-neutral component that must
131/// finish before recording `server_shutdown`. The audit-manager flush still runs
132/// after `AUDIT_LOGS_COMPONENT`.
133pub fn audit_component_after<T, StartFn, StartFut, ShutdownFn, ShutdownFut, FlushFn, FlushFut>(
134    resources: T,
135    shutdown_dependencies: &'static [&'static str],
136    record_server_start: StartFn,
137    record_server_shutdown: ShutdownFn,
138    flush_audit_manager: FlushFn,
139) -> RuntimeComponentBundleRegistration<impl aster_forge_runtime::RuntimeComponentBundle>
140where
141    T: Clone + Send + 'static,
142    StartFn: FnOnce(T) -> StartFut + Send + Sync + 'static,
143    StartFut: Future<Output = Result<(), String>> + Send + 'static,
144    ShutdownFn: FnOnce(T) -> ShutdownFut + Send + Sync + 'static,
145    ShutdownFut: Future<Output = Result<(), String>> + Send + 'static,
146    FlushFn: FnOnce(()) -> FlushFut + Send + Sync + 'static,
147    FlushFut: Future<Output = Result<(), String>> + Send + 'static,
148{
149    let startup_resources = resources.clone();
150    let server_start = server_start_audit_component(startup_resources, record_server_start);
151    let server_shutdown = server_shutdown_audit_component_after(
152        resources,
153        shutdown_dependencies,
154        record_server_shutdown,
155    );
156    let audit_manager = audit_manager_component(flush_audit_manager);
157
158    runtime_component(move |registry: &mut RuntimeComponentRegistry| {
159        registry
160            .register_bundle(server_start)
161            .register_bundle(server_shutdown)
162            .register_bundle(audit_manager);
163    })
164}
165
166/// Creates the full audit lifecycle component with caller-provided shutdown
167/// dependencies for hooks that cannot fail.
168///
169/// Use this instead of [`audit_component_infallible`] when a product needs a
170/// shutdown dependency other than the crate default.
171pub fn audit_component_after_infallible<
172    T,
173    StartFn,
174    StartFut,
175    ShutdownFn,
176    ShutdownFut,
177    FlushFn,
178    FlushFut,
179>(
180    resources: T,
181    shutdown_dependencies: &'static [&'static str],
182    record_server_start: StartFn,
183    record_server_shutdown: ShutdownFn,
184    flush_audit_manager: FlushFn,
185) -> RuntimeComponentBundleRegistration<impl aster_forge_runtime::RuntimeComponentBundle>
186where
187    T: Clone + Send + 'static,
188    StartFn: FnOnce(T) -> StartFut + Send + Sync + 'static,
189    StartFut: Future<Output = ()> + Send + 'static,
190    ShutdownFn: FnOnce(T) -> ShutdownFut + Send + Sync + 'static,
191    ShutdownFut: Future<Output = ()> + Send + 'static,
192    FlushFn: FnOnce(()) -> FlushFut + Send + Sync + 'static,
193    FlushFut: Future<Output = ()> + Send + 'static,
194{
195    audit_component_after(
196        resources,
197        shutdown_dependencies,
198        move |resources| async move {
199            record_server_start(resources).await;
200            Ok(())
201        },
202        move |resources| async move {
203            record_server_shutdown(resources).await;
204            Ok(())
205        },
206        move |()| async move {
207            flush_audit_manager(()).await;
208            Ok(())
209        },
210    )
211}
212
213/// Creates the audit shutdown component bundle without the server-start phase.
214///
215/// Use this only when a product intentionally records server startup elsewhere.
216/// Normal Aster services should use [`audit_component`] so startup, shutdown,
217/// and manager flush share one lifecycle component. With the
218/// `mail-outbox-dependency` feature enabled, the shutdown audit phase
219/// automatically runs after the mail outbox drain component.
220pub fn shutdown_audit_component<T, RecordFn, RecordFut, FlushFn, FlushFut>(
221    resources: T,
222    record_server_shutdown: RecordFn,
223    flush_audit_manager: FlushFn,
224) -> RuntimeComponentBundleRegistration<impl aster_forge_runtime::RuntimeComponentBundle>
225where
226    T: Send + 'static,
227    RecordFn: FnOnce(T) -> RecordFut + Send + Sync + 'static,
228    RecordFut: Future<Output = Result<(), String>> + Send + 'static,
229    FlushFn: FnOnce(()) -> FlushFut + Send + Sync + 'static,
230    FlushFut: Future<Output = Result<(), String>> + Send + 'static,
231{
232    shutdown_audit_component_after(
233        resources,
234        DEFAULT_SERVER_SHUTDOWN_AUDIT_DEPENDENCIES,
235        record_server_shutdown,
236        flush_audit_manager,
237    )
238}
239
240/// Creates the audit shutdown component bundle with caller-provided dependencies
241/// for the server-shutdown audit phase.
242pub fn shutdown_audit_component_after<T, RecordFn, RecordFut, FlushFn, FlushFut>(
243    resources: T,
244    shutdown_dependencies: &'static [&'static str],
245    record_server_shutdown: RecordFn,
246    flush_audit_manager: FlushFn,
247) -> RuntimeComponentBundleRegistration<impl aster_forge_runtime::RuntimeComponentBundle>
248where
249    T: Send + 'static,
250    RecordFn: FnOnce(T) -> RecordFut + Send + Sync + 'static,
251    RecordFut: Future<Output = Result<(), String>> + Send + 'static,
252    FlushFn: FnOnce(()) -> FlushFut + Send + Sync + 'static,
253    FlushFut: Future<Output = Result<(), String>> + Send + 'static,
254{
255    let server_shutdown = server_shutdown_audit_component_after(
256        resources,
257        shutdown_dependencies,
258        record_server_shutdown,
259    );
260    let audit_manager = audit_manager_component(flush_audit_manager);
261
262    runtime_component(move |registry: &mut RuntimeComponentRegistry| {
263        registry
264            .register_bundle(server_shutdown)
265            .register_bundle(audit_manager);
266    })
267}
268
269/// Creates the server-start audit startup component.
270pub fn server_start_audit_component<T, F, Fut>(
271    resources: T,
272    record_server_start: F,
273) -> RuntimeComponentBundleRegistration<impl aster_forge_runtime::RuntimeComponentBundle>
274where
275    T: Send + 'static,
276    F: FnOnce(T) -> Fut + Send + Sync + 'static,
277    Fut: Future<Output = Result<(), String>> + Send + 'static,
278{
279    runtime_component(move |registry: &mut RuntimeComponentRegistry| {
280        let mut resources = Some(resources);
281        let mut record_server_start = Some(record_server_start);
282        registry.component_startup(
283            AUDIT_LOGS_COMPONENT,
284            RuntimeComponentKind::Product,
285            SERVER_START_AUDIT_PHASE,
286            StartupPhaseFailurePolicy::Required,
287            move || {
288                let resources = resources.take();
289                let record_server_start = record_server_start.take();
290                async move {
291                    let Some(resources) = resources else {
292                        return Err(
293                            "server start audit startup phase resources already consumed"
294                                .to_string(),
295                        );
296                    };
297                    let Some(record_server_start) = record_server_start else {
298                        return Err("server start audit startup phase callback already consumed"
299                            .to_string());
300                    };
301                    record_server_start(resources).await
302                }
303            },
304        );
305    })
306}
307
308/// Creates the server-shutdown audit component with the crate's default
309/// dependencies.
310///
311/// The default dependency list is empty unless the `mail-outbox-dependency`
312/// feature is enabled.
313pub fn server_shutdown_audit_component<T, F, Fut>(
314    resources: T,
315    record_server_shutdown: F,
316) -> RuntimeComponentBundleRegistration<aster_forge_runtime::ShutdownResourceComponent<T>>
317where
318    T: Send + 'static,
319    F: FnOnce(T) -> Fut + Send + Sync + 'static,
320    Fut: Future<Output = Result<(), String>> + Send + 'static,
321{
322    server_shutdown_audit_component_after(
323        resources,
324        DEFAULT_SERVER_SHUTDOWN_AUDIT_DEPENDENCIES,
325        record_server_shutdown,
326    )
327}
328
329/// Creates the server-shutdown audit component with caller-provided dependencies.
330pub fn server_shutdown_audit_component_after<T, F, Fut>(
331    resources: T,
332    shutdown_dependencies: &'static [&'static str],
333    record_server_shutdown: F,
334) -> RuntimeComponentBundleRegistration<aster_forge_runtime::ShutdownResourceComponent<T>>
335where
336    T: Send + 'static,
337    F: FnOnce(T) -> Fut + Send + Sync + 'static,
338    Fut: Future<Output = Result<(), String>> + Send + 'static,
339{
340    aster_forge_runtime::shutdown_resource_component_after(
341        AUDIT_LOGS_COMPONENT,
342        RuntimeComponentKind::Product,
343        SERVER_SHUTDOWN_AUDIT_PHASE,
344        shutdown_dependencies,
345        resources,
346        record_server_shutdown,
347    )
348}
349
350/// Creates the audit-manager flush component.
351pub fn audit_manager_component<F, Fut>(
352    flush_audit_manager: F,
353) -> RuntimeComponentBundleRegistration<aster_forge_runtime::ShutdownResourceComponent<()>>
354where
355    F: FnOnce(()) -> Fut + Send + Sync + 'static,
356    Fut: Future<Output = Result<(), String>> + Send + 'static,
357{
358    aster_forge_runtime::shutdown_resource_component_after(
359        AUDIT_MANAGER_COMPONENT,
360        RuntimeComponentKind::Product,
361        AUDIT_MANAGER_FLUSH_SHUTDOWN_PHASE,
362        &[AUDIT_LOGS_COMPONENT],
363        (),
364        flush_audit_manager,
365    )
366}
367
368#[cfg(test)]
369mod tests {
370    use std::sync::{
371        Arc,
372        atomic::{AtomicUsize, Ordering},
373    };
374
375    use aster_forge_runtime::RuntimeComponentBundle;
376
377    use super::{
378        AUDIT_LOGS_COMPONENT, AUDIT_MANAGER_COMPONENT, AUDIT_MANAGER_FLUSH_SHUTDOWN_PHASE,
379        SERVER_SHUTDOWN_AUDIT_PHASE, SERVER_START_AUDIT_PHASE, audit_component,
380        audit_component_after, audit_component_after_infallible, audit_component_infallible,
381        audit_manager_component, server_shutdown_audit_component,
382        server_shutdown_audit_component_after, server_start_audit_component,
383        shutdown_audit_component,
384    };
385
386    #[cfg(not(feature = "mail-outbox-dependency"))]
387    #[test]
388    fn server_shutdown_audit_component_registers_without_dependencies() {
389        let registry = aster_forge_runtime::RuntimeComponentRegistry::configured(|registry| {
390            server_shutdown_audit_component((), |()| async { Ok(()) }).register(registry);
391        });
392
393        let descriptor = registry
394            .descriptor(AUDIT_LOGS_COMPONENT)
395            .expect("audit logs component should be registered");
396        assert_eq!(
397            descriptor.kind,
398            aster_forge_runtime::RuntimeComponentKind::Product
399        );
400        assert!(descriptor.dependencies.is_empty());
401        assert_eq!(
402            descriptor
403                .shutdown
404                .first()
405                .expect("audit logs shutdown should be registered")
406                .phase_name,
407            SERVER_SHUTDOWN_AUDIT_PHASE
408        );
409    }
410
411    #[cfg(feature = "mail-outbox-dependency")]
412    #[test]
413    fn server_shutdown_audit_component_registers_mail_outbox_dependency() {
414        let registry = aster_forge_runtime::RuntimeComponentRegistry::configured(|registry| {
415            server_shutdown_audit_component((), |()| async { Ok(()) }).register(registry);
416        });
417
418        let descriptor = registry
419            .descriptor(AUDIT_LOGS_COMPONENT)
420            .expect("audit logs component should be registered");
421        assert_eq!(
422            descriptor.kind,
423            aster_forge_runtime::RuntimeComponentKind::Product
424        );
425        assert_eq!(
426            descriptor.dependencies,
427            vec![aster_forge_mail::MAIL_OUTBOX_COMPONENT]
428        );
429        assert_eq!(
430            descriptor
431                .shutdown
432                .first()
433                .expect("audit logs shutdown should be registered")
434                .phase_name,
435            SERVER_SHUTDOWN_AUDIT_PHASE
436        );
437    }
438
439    #[test]
440    fn server_shutdown_audit_component_after_registers_caller_dependencies() {
441        let registry = aster_forge_runtime::RuntimeComponentRegistry::configured(|registry| {
442            server_shutdown_audit_component_after(
443                (),
444                &["background_tasks", "mail_outbox"],
445                |()| async { Ok(()) },
446            )
447            .register(registry);
448        });
449
450        let descriptor = registry
451            .descriptor(AUDIT_LOGS_COMPONENT)
452            .expect("audit logs component should be registered");
453        assert_eq!(
454            descriptor.dependencies,
455            vec!["background_tasks", "mail_outbox"]
456        );
457    }
458
459    #[tokio::test]
460    async fn server_start_audit_component_registers_and_runs_startup_phase() {
461        let calls = Arc::new(AtomicUsize::new(0));
462        let calls_for_start = calls.clone();
463        let mut registry = aster_forge_runtime::RuntimeComponentRegistry::configured(|registry| {
464            server_start_audit_component((), move |()| {
465                let calls = calls_for_start.clone();
466                async move {
467                    calls.fetch_add(1, Ordering::SeqCst);
468                    Ok(())
469                }
470            })
471            .register(registry);
472        });
473
474        let descriptor = registry
475            .descriptor(AUDIT_LOGS_COMPONENT)
476            .expect("audit logs component should be registered");
477        assert_eq!(descriptor.startup.len(), 1);
478        assert_eq!(descriptor.startup[0].phase_name, SERVER_START_AUDIT_PHASE);
479
480        let report = registry.startup().await;
481
482        assert!(!report.aborted());
483        assert_eq!(calls.load(Ordering::SeqCst), 1);
484    }
485
486    #[test]
487    fn audit_manager_component_registers_after_audit_logs() {
488        let registry = aster_forge_runtime::RuntimeComponentRegistry::configured(|registry| {
489            audit_manager_component(|()| async { Ok(()) }).register(registry);
490        });
491
492        let descriptor = registry
493            .descriptor(AUDIT_MANAGER_COMPONENT)
494            .expect("audit manager component should be registered");
495        assert_eq!(descriptor.dependencies, vec![AUDIT_LOGS_COMPONENT]);
496        assert_eq!(
497            descriptor
498                .shutdown
499                .first()
500                .expect("audit manager shutdown should be registered")
501                .phase_name,
502            AUDIT_MANAGER_FLUSH_SHUTDOWN_PHASE
503        );
504    }
505
506    #[tokio::test]
507    async fn audit_component_registers_startup_shutdown_and_manager_flush() {
508        let order = Arc::new(std::sync::Mutex::new(Vec::new()));
509        let order_for_start = order.clone();
510        let order_for_record = order.clone();
511        let order_for_flush = order.clone();
512
513        let mut registry = aster_forge_runtime::RuntimeComponentRegistry::configured(|registry| {
514            audit_component(
515                (),
516                move |()| {
517                    let order = order_for_start.clone();
518                    async move {
519                        order
520                            .lock()
521                            .expect("audit component test order should lock")
522                            .push("start");
523                        Ok(())
524                    }
525                },
526                move |()| {
527                    let order = order_for_record.clone();
528                    async move {
529                        order
530                            .lock()
531                            .expect("audit component test order should lock")
532                            .push("record");
533                        Ok(())
534                    }
535                },
536                move |()| {
537                    let order = order_for_flush.clone();
538                    async move {
539                        order
540                            .lock()
541                            .expect("audit component test order should lock")
542                            .push("flush");
543                        Ok(())
544                    }
545                },
546            )
547            .register(registry);
548        });
549
550        let startup_report = registry.startup().await;
551        assert!(!startup_report.aborted());
552        let shutdown_report = registry.shutdown().await;
553
554        assert!(!shutdown_report.has_failures());
555        assert_eq!(
556            order
557                .lock()
558                .expect("audit component test order should lock")
559                .as_slice(),
560            ["start", "record", "flush"]
561        );
562    }
563
564    #[tokio::test]
565    async fn infallible_audit_component_runs_unit_returning_hooks_in_order() {
566        let order = Arc::new(std::sync::Mutex::new(Vec::new()));
567        let order_for_start = order.clone();
568        let order_for_record = order.clone();
569        let order_for_flush = order.clone();
570
571        let mut registry = aster_forge_runtime::RuntimeComponentRegistry::configured(|registry| {
572            audit_component_infallible(
573                (),
574                move |()| {
575                    let order = order_for_start.clone();
576                    async move {
577                        order
578                            .lock()
579                            .expect("audit component test order should lock")
580                            .push("start");
581                    }
582                },
583                move |()| {
584                    let order = order_for_record.clone();
585                    async move {
586                        order
587                            .lock()
588                            .expect("audit component test order should lock")
589                            .push("record");
590                    }
591                },
592                move |()| {
593                    let order = order_for_flush.clone();
594                    async move {
595                        order
596                            .lock()
597                            .expect("audit component test order should lock")
598                            .push("flush");
599                    }
600                },
601            )
602            .register(registry);
603        });
604
605        let startup_report = registry.startup().await;
606        assert!(!startup_report.aborted());
607        let shutdown_report = registry.shutdown().await;
608
609        assert!(!shutdown_report.has_failures());
610        assert_eq!(
611            order
612                .lock()
613                .expect("audit component test order should lock")
614                .as_slice(),
615            ["start", "record", "flush"]
616        );
617    }
618
619    #[test]
620    fn audit_component_after_registers_shutdown_dependencies() {
621        let registry = aster_forge_runtime::RuntimeComponentRegistry::configured(|registry| {
622            audit_component_after(
623                (),
624                &["mail_outbox"],
625                |()| async { Ok(()) },
626                |()| async { Ok(()) },
627                |()| async { Ok(()) },
628            )
629            .register(registry);
630        });
631
632        let descriptor = registry
633            .descriptor(AUDIT_LOGS_COMPONENT)
634            .expect("audit logs component should be registered");
635        assert_eq!(descriptor.dependencies, vec!["mail_outbox"]);
636    }
637
638    #[test]
639    fn infallible_audit_component_after_registers_shutdown_dependencies() {
640        let registry = aster_forge_runtime::RuntimeComponentRegistry::configured(|registry| {
641            audit_component_after_infallible(
642                (),
643                &["background_tasks"],
644                |()| async {},
645                |()| async {},
646                |()| async {},
647            )
648            .register(registry);
649        });
650
651        let descriptor = registry
652            .descriptor(AUDIT_LOGS_COMPONENT)
653            .expect("audit logs component should be registered");
654        assert_eq!(descriptor.dependencies, vec!["background_tasks"]);
655    }
656
657    #[cfg(feature = "mail-outbox-dependency")]
658    #[test]
659    fn audit_component_registers_mail_outbox_dependency_when_feature_enabled() {
660        let registry = aster_forge_runtime::RuntimeComponentRegistry::configured(|registry| {
661            audit_component(
662                (),
663                |()| async { Ok(()) },
664                |()| async { Ok(()) },
665                |()| async { Ok(()) },
666            )
667            .register(registry);
668        });
669
670        let descriptor = registry
671            .descriptor(AUDIT_LOGS_COMPONENT)
672            .expect("audit logs component should be registered");
673        assert_eq!(
674            descriptor.dependencies,
675            vec![aster_forge_mail::MAIL_OUTBOX_COMPONENT]
676        );
677    }
678
679    #[tokio::test]
680    async fn shutdown_audit_component_runs_shutdown_record_before_manager_flush() {
681        let order = Arc::new(std::sync::Mutex::new(Vec::new()));
682        let order_for_record = order.clone();
683        let order_for_flush = order.clone();
684
685        let mut registry = aster_forge_runtime::RuntimeComponentRegistry::new();
686        registry.register_bundle(shutdown_audit_component(
687            (),
688            move |()| {
689                let order = order_for_record.clone();
690                async move {
691                    order
692                        .lock()
693                        .expect("audit component test order should lock")
694                        .push("record");
695                    Ok(())
696                }
697            },
698            move |()| {
699                let order = order_for_flush.clone();
700                async move {
701                    order
702                        .lock()
703                        .expect("audit component test order should lock")
704                        .push("flush");
705                    Ok(())
706                }
707            },
708        ));
709        let report = registry.shutdown().await;
710
711        assert!(!report.has_failures());
712        assert_eq!(
713            order
714                .lock()
715                .expect("audit component test order should lock")
716                .as_slice(),
717            ["record", "flush"]
718        );
719    }
720}