1/*
  2 * SPDX-FileCopyrightText: 2026 Copyright (c) Contributors to the Eclipse Foundation
  3 *
  4 * See the NOTICE file(s) distributed with this work for additional
  5 * information regarding copyright ownership.
  6 *
  7 * This program and the accompanying materials are made available under the
  8 * terms of the Apache License Version 2.0 which is available at
  9 * https://www.apache.org/licenses/LICENSE-2.0
 10 *
 11 * SPDX-License-Identifier: Apache-2.0
 12 */
 13
 14//! CDA startup builder.
 15//!
 16//! [`Setup`] is the public entry-point for applications that need to customize how the CDA
 17//! boots. The default startup path uses [`Setup::new`] with the standard update plugin; a
 18//! custom path calls [`Setup::with_update_plugin`] to inject any [`RuntimeFilesUpdatePlugin`]
 19//! implementation and [`Setup::with_communication_plugin`] to replace the default
 20//! `init_mode`-aware communication plugin, before handing the `Setup` to one of the
 21//! `run_*` functions.
 22//! Lock-priority policies are selected with [`Setup::with_lock_priority_plugin`].
 23//!
 24//! [`RuntimeFilesUpdatePlugin`]: cda_interfaces::runtime_update_api::RuntimeFilesUpdatePlugin
 25
 26use std::{sync::Arc, time::Duration};
 27
 28use cda_comm_can::CanDiagGateway;
 29use cda_comm_doip::DoipDiagGateway;
 30use cda_core::EcuManager;
 31use cda_interfaces::{
 32    HashMap, ShutdownSignal,
 33    communication_control::{
 34        ActivationCause, CommunicationAccess, CommunicationInitMode, CommunicationLifecycle,
 35        CommunicationVariantDetection, PostUpdateCommunicationMode, VariantDetectionMode,
 36        error::CommControlError,
 37    },
 38    component_slot::{ComponentSlot, ReplaceComponent},
 39    health::HealthProvider,
 40    http_protection::registry::HttpProtectionRegistry,
 41    lock_priority_api::LockPriorityPolicy,
 42};
 43use cda_plugin_communication_management::{
 44    lifecycle::{
 45        CommunicationRuntime,
 46        access::CommunicationAccessView,
 47        build_communication_runtime,
 48        disable::{CommunicationDisableView, DisableCommunication},
 49    },
 50    plugin::{
 51        CommunicationPlugin, CommunicationPluginBuilder, default::DefaultCommunicationPluginBuilder,
 52    },
 53};
 54use cda_plugin_security::{SecurityPlugin, SecurityPluginLoader};
 55use cda_storage::LocalStorage;
 56use cda_transport_router::DiagnosticTransportRouter;
 57use futures::future::BoxFuture;
 58use tokio::sync::RwLock;
 59
 60use crate::{
 61    ApplicationState,
 62    config::configfile::Configuration,
 63    error::AppError,
 64    update::{UpdatePluginBuilder, add_runtime_update_routes},
 65    vehicle::{UdsManagerType, VehicleData},
 66};
 67
 68// Type alias
 69
 70/// Boxed async callback executed after the webserver starts but **before** vehicle data is
 71/// loaded. Receives the dynamic router so it can register early routes.
 72pub(crate) type PreLoadHook = Box<
 73    dyn FnOnce(cda_sovd::dynamic_router::DynamicRouter) -> BoxFuture<'static, Result<(), AppError>>
 74        + Send,
 75>;
 76
 77/// Runtime context produced during CDA initialization and provided to update-plugin builders.
 78///
 79/// `gateway_replacer` and `uds_manager_replacer` are replace-only. An update plugin can
 80/// install a freshly built component, but has no read access, and therefore no operational
 81/// authority, over the live gateway or UDS manager.
 82pub struct CdaRuntime<SP: SecurityPlugin> {
 83    pub config: Arc<RwLock<Configuration>>,
 84    pub uds_manager_replacer: Arc<dyn ReplaceComponent<UdsManagerType<SP>>>,
 85    pub gateway_replacer: Arc<
 86        dyn ReplaceComponent<
 87            DiagnosticTransportRouter<DoipDiagGateway<EcuManager<SP>>, CanDiagGateway>,
 88        >,
 89    >,
 90    pub dynamic_router: cda_sovd::dynamic_router::DynamicRouter,
 91    pub vehicle_route_handle: cda_sovd::RouteHandle,
 92    pub lock_provider: Arc<cda_sovd::SovdLockStateProvider>,
 93    pub flash_files_path: String,
 94    pub flash_functional_class_default: String,
 95    pub components_config: cda_interfaces::datatypes::ComponentsConfig,
 96    pub health: Option<HashMap<String, Arc<dyn HealthProvider>>>,
 97    pub storage: Arc<LocalStorage>,
 98    pub mdd_decompress: bool,
 99    pub shutdown_signal: ShutdownSignal,
100    /// Transport behavior to restore after a runtime database update completes.
101    pub post_update_mode: PostUpdateCommunicationMode,
102    /// Narrow handle used by update plugins to request activation without
103    /// gaining the full [`CommunicationPlugin`](CommunicationPlugin) lifecycle authority.
104    pub communication_access: Arc<dyn CommunicationAccess>,
105    /// Narrow handle used by update plugins to take exclusive ownership of
106    /// communication for the duration of a runtime file update.
107    pub communication_disable: Arc<dyn DisableCommunication>,
108    /// Shared registry of HTTP request restrictions installed by update plugins.
109    pub http_protections: HttpProtectionRegistry,
110    /// Retry hint surfaced on the HTTP `Retry-After` header while a runtime
111    /// update holds communication exclusively.
112    pub update_retry_after: Duration,
113}
114
115/// Bridges the `RwLock`-wrapped `UdsManager` to both communication-framework
116/// hook traits. The `UdsManager` implements each half itself, so this only
117/// forwards through the lock.
118struct UdsCommunicationHooks<SP: SecurityPlugin> {
119    uds_manager: ComponentSlot<UdsManagerType<SP>>,
120}
121
122#[async_trait::async_trait]
123impl<SP: SecurityPlugin> CommunicationLifecycle for UdsCommunicationHooks<SP> {
124    fn name(&self) -> &'static str {
125        "variant-detection-listener"
126    }
127
128    async fn initialize(&self) -> Result<(), CommControlError> {
129        self.uds_manager.read().await.initialize().await
130    }
131
132    async fn on_enabled(&self) {
133        self.uds_manager.read().await.on_enabled().await;
134    }
135
136    async fn deinitialize(&self) {
137        self.uds_manager.read().await.deinitialize().await;
138    }
139}
140
141#[async_trait::async_trait]
142impl<SP: SecurityPlugin> CommunicationVariantDetection for UdsCommunicationHooks<SP> {
143    fn name(&self) -> &'static str {
144        "variant-detection"
145    }
146
147    async fn detect(&self) -> Result<(), CommControlError> {
148        self.uds_manager.read().await.detect().await
149    }
150}
151
152/// Builder for customizing the CDA startup sequence.
153///
154/// Use [`Setup::new`] to start with the defaults and then chain optional
155/// configuration methods:
156///
157/// ```rust,ignore
158/// use opensovd_cda_lib::{
159///     setup::Setup,
160///     update::update_plugin_fn,
161/// };
162///
163/// let setup = Setup::<MySecurityPlugin, MySecurityLoader>::new()
164///     .with_preload(|router| async move {
165///         // register additional routes before databases load
166///         Ok(())
167///     })
168///     .with_update_plugin(update_plugin_fn(|infra| async move {
169///         Ok(MyCustomUpdatePlugin::new(infra))
170///     }))
171///     .with_communication_plugin(MyCommunicationPluginBuilder);
172///
173/// opensovd_cda_lib::run_with_ext_from_config(config, setup).await?;
174/// ```
175///
176/// When no update plugin is configured (`UpdatePluginBuilder = ()`) the runtime-update routes
177/// are **not** mounted. Pass a builder, typically via [`update_plugin_fn`] or by implementing
178/// [`UpdatePluginBuilder`], to enable them.
179///
180/// [`update_plugin_fn`]: crate::update::update_plugin_fn
181/// [`UpdatePluginBuilder`]: UpdatePluginBuilder
182///
183/// Type parameters:
184/// - `SP`: security plugin
185/// - `SL`: security plugin loader
186/// - `UPB`: update plugin builder, defaults to `()`
187/// - `CPB`: communication plugin builder, defaults to [`DefaultCommunicationPluginBuilder`]
188pub struct Setup<
189    SP: SecurityPlugin,
190    SL: SecurityPluginLoader,
191    UPB = (),
192    CPB = DefaultCommunicationPluginBuilder,
193> {
194    pub(crate) _phantom: std::marker::PhantomData<(SP, SL)>,
195    /// Optional callback run after the webserver starts but before vehicle data is loaded.
196    pub(crate) pre_load: Option<PreLoadHook>,
197    /// Optional update-plugin builder. When `None` (or `UPB = ()`) the runtime-update
198    /// routes are not registered.
199    pub(crate) build_update_plugin: Option<UPB>,
200    pub(crate) build_communication_plugin: CPB,
201    pub(crate) initialize_tracing: bool,
202    pub(crate) shutdown_signal: Option<ShutdownSignal>,
203    /// Lock-priority policy fixed for the lifetime of the lock store.
204    pub(crate) lock_priority_policy: Arc<dyn LockPriorityPolicy>,
205}
206
207impl<SP: SecurityPlugin, SL: SecurityPluginLoader> Default for Setup<SP, SL> {
208    fn default() -> Self {
209        Self::new()
210    }
211}
212
213impl<SP: SecurityPlugin, SL: SecurityPluginLoader> Setup<SP, SL> {
214    /// Creates a new `Setup` with no preload hook or update plugin and no lock preemption.
215    #[must_use]
216    pub fn new() -> Self {
217        Self {
218            _phantom: std::marker::PhantomData,
219            pre_load: None,
220            build_update_plugin: None,
221            build_communication_plugin: DefaultCommunicationPluginBuilder,
222            initialize_tracing: true,
223            shutdown_signal: None,
224            lock_priority_policy: Arc::new(cda_plugin_lock_priority::NoPreemptionPolicy),
225        }
226    }
227}
228
229impl<SP: SecurityPlugin, SL: SecurityPluginLoader, UPB, CPB> Setup<SP, SL, UPB, CPB> {
230    /// Uses the tracing subscriber already installed by the embedding process.
231    #[must_use]
232    pub fn with_existing_tracing(mut self) -> Self {
233        self.initialize_tracing = false;
234        self
235    }
236
237    /// Uses an externally managed shutdown signal instead of process signals.
238    #[must_use]
239    pub fn with_shutdown_signal(mut self, shutdown_signal: ShutdownSignal) -> Self {
240        self.shutdown_signal = Some(shutdown_signal);
241        self
242    }
243
244    /// Registers a preload hook.
245    ///
246    /// The hook is called after the webserver starts, health state and sd-notify are
247    /// configured, but **before** vehicle data (MDD databases) are loaded.  Use it to
248    /// mount additional routes that should be reachable during the (potentially slow)
249    /// database load.
250    ///
251    /// The hook must return `Ok(())` to allow startup to continue; returning `Err` aborts
252    /// the entire startup.
253    #[must_use]
254    pub fn with_preload<F, Fut>(mut self, f: F) -> Self
255    where
256        F: FnOnce(cda_sovd::dynamic_router::DynamicRouter) -> Fut + Send + 'static,
257        Fut: Future<Output = Result<(), AppError>> + Send + 'static,
258    {
259        self.pre_load = Some(Box::new(|router| Box::pin(f(router))));
260        self
261    }
262
263    /// Configures a custom runtime update plugin.
264    ///
265    /// `builder` will be called after vehicle data is loaded and routes are registered. It
266    /// receives the complete [`CdaRuntime`] context so it can access the UDS manager, `DoIP`
267    /// gateway, storage directory, lock provider, and every other piece of infrastructure it
268    /// needs.
269    ///
270    /// The returned plugin is wrapped in [`ExclusiveRuntimePlugin`] (read/write mutual
271    /// exclusion) and mounted on the standard runtime-update HTTP endpoints automatically.
272    ///
273    /// Use [`update_plugin_fn`] to wrap a plain async closure without implementing the trait
274    /// manually:
275    ///
276    /// ```rust,ignore
277    /// setup.with_update_plugin(update_plugin_fn(|infra| async move {
278    ///     Ok(MyPlugin::new(infra))
279    /// }))
280    /// ```
281    ///
282    /// [`ExclusiveRuntimePlugin`]: cda_interfaces::runtime_update_api::ExclusiveRuntimePlugin
283    /// [`update_plugin_fn`]: crate::update::update_plugin_fn
284    pub fn with_update_plugin<UPB2: UpdatePluginBuilder<SP>>(
285        self,
286        builder: UPB2,
287    ) -> Setup<SP, SL, UPB2, CPB> {
288        Setup {
289            _phantom: self._phantom,
290            pre_load: self.pre_load,
291            build_update_plugin: Some(builder),
292            build_communication_plugin: self.build_communication_plugin,
293            initialize_tracing: self.initialize_tracing,
294            shutdown_signal: self.shutdown_signal,
295            lock_priority_policy: self.lock_priority_policy,
296        }
297    }
298
299    /// Replaces the default communication plugin factory.
300    ///
301    /// The factory is invoked after the passive transport and authoritative manager exist.
302    pub fn with_communication_plugin<CPB2: CommunicationPluginBuilder>(
303        self,
304        plugin: CPB2,
305    ) -> Setup<SP, SL, UPB, CPB2> {
306        Setup {
307            _phantom: self._phantom,
308            pre_load: self.pre_load,
309            build_update_plugin: self.build_update_plugin,
310            build_communication_plugin: plugin,
311            initialize_tracing: self.initialize_tracing,
312            shutdown_signal: self.shutdown_signal,
313            lock_priority_policy: self.lock_priority_policy,
314        }
315    }
316
317    /// Registers a lock-priority policy.
318    ///
319    /// Policy implementations receive revisioned
320    /// [`LockPriorityEvaluation`](cda_interfaces::lock_priority_api::LockPriorityEvaluation)
321    /// snapshots and return a
322    /// [`LockPriorityDecision`](cda_interfaces::lock_priority_api::LockPriorityDecision). Policy
323    /// calls are bounded by configured timeout and stale-state retries; returning `Preempt` also
324    /// requires the request's `break_lock` flag.
325    #[must_use]
326    pub fn with_lock_priority_plugin(mut self, plugin: Arc<dyn LockPriorityPolicy>) -> Self {
327        self.lock_priority_policy = plugin;
328        self
329    }
330}
331
332/// Installs the global HTTP protection guard, builds the authoritative communication plugin
333/// runtime, and derives a narrow access view for UDS manager construction.
334///
335/// Diagnostic operations request activation directly (see
336/// `CommunicationAccess::request_activate`), so `http_protections` is left to the
337/// runtime-update plugin's own protection.
338async fn build_communication_runtime_and_access<SP, CPB>(
339    ws: &ApplicationState,
340    gateway: ComponentSlot<
341        DiagnosticTransportRouter<DoipDiagGateway<EcuManager<SP>>, CanDiagGateway>,
342    >,
343    communication_plugin: CPB,
344    init_mode: CommunicationInitMode,
345    variant_detection: VariantDetectionMode,
346    retry_after: Duration,
347    http_protections: &HttpProtectionRegistry,
348) -> Result<(CommunicationRuntime, Arc<dyn CommunicationAccess>), AppError>
349where
350    SP: SecurityPlugin,
351    CPB: CommunicationPluginBuilder,
352{
353    cda_sovd::install_http_restriction_guard(
354        &ws.dynamic_router,
355        Arc::new(http_protections.clone()),
356    )
357    .await;
358
359    let transport_control = gateway.transport_control();
360    let communication_runtime = build_communication_runtime(
361        communication_plugin,
362        transport_control,
363        init_mode,
364        variant_detection,
365    )
366    .await
367    .map_err(|error| AppError::InitializationFailed(error.to_string()))?;
368
369    let communication_access: Arc<dyn CommunicationAccess> = Arc::new(
370        CommunicationAccessView::new(Arc::clone(&communication_runtime.plugin), retry_after),
371    );
372
373    Ok((communication_runtime, communication_access))
374}
375
376/// Registers the UDS manager's two communication hooks through the authoritative
377/// plugin.
378///
379/// Both halves come from one adapter but register separately. The listener runs
380/// on every transport enable, while detection is the optional last stage that
381/// `trigger_detection()` can run on its own.
382async fn register_communication_hooks<SP>(
383    plugin: &Arc<dyn CommunicationPlugin>,
384    uds_manager: &ComponentSlot<UdsManagerType<SP>>,
385) -> Result<(), AppError>
386where
387    SP: SecurityPlugin,
388{
389    let hooks = Arc::new(UdsCommunicationHooks {
390        uds_manager: uds_manager.clone(),
391    });
392    plugin
393        .register_lifecycle_hook(Arc::clone(&hooks) as Arc<dyn CommunicationLifecycle>)
394        .await
395        .map_err(|error| AppError::InitializationFailed(error.to_string()))?;
396    plugin
397        .register_variant_detection(hooks as Arc<dyn CommunicationVariantDetection>)
398        .await
399        .map_err(|error| AppError::InitializationFailed(error.to_string()))
400}
401
402/// `Always` initializes whole-vehicle communication eagerly at startup and propagates
403/// failure according to existing application-start semantics. `OnDemand` and `Disabled`
404/// leave it uninitialized, with HTTP/SOVD already served by the routes registered
405/// beforehand. Under `OnDemand` an explicit `activate()` or a qualifying ECU request
406/// initializes it. The default plugin offers no activation path under `Disabled`.
[docs]407/// [[ dimpl~deferred-communication-startup, Deferred communication startup wiring, dimpl ]]
408async fn activate_communication_per_init_mode(
409    plugin: &Arc<dyn CommunicationPlugin>,
410    init_mode: CommunicationInitMode,
411) -> Result<(), AppError> {
412    match init_mode {
413        CommunicationInitMode::Always => plugin
414            .activate(ActivationCause::Startup)
415            .await
416            .map(|_| ())
417            .map_err(AppError::from),
418        CommunicationInitMode::OnDemand | CommunicationInitMode::Disabled => Ok(()),
419    }
420}
421
422/// Builds the caller-supplied update plugin, if any, and mounts its HTTP routes.
423async fn setup_update_plugin<SP, SL, UPB>(
424    ws: &ApplicationState,
425    build_update_plugin: Option<UPB>,
426    infra: CdaRuntime<SP>,
427    lock_provider: Arc<cda_sovd::SovdLockStateProvider>,
428    upload_body_limit_bytes: usize,
429    update_retry_after: Duration,
430) -> Result<(), AppError>
431where
432    SP: SecurityPlugin,
433    SL: SecurityPluginLoader,
434    UPB: UpdatePluginBuilder<SP>,
435{
436    let Some(builder) = build_update_plugin else {
437        return Ok(());
438    };
439
440    let plugin = builder.build(infra).await?;
441    add_runtime_update_routes::<SL, _>(
442        &ws.dynamic_router,
443        plugin,
444        lock_provider,
445        upload_body_limit_bytes,
446        update_retry_after,
447    )
448    .await;
449
450    Ok(())
451}
452
453/// Registers all SOVD routes, the runtime-update plugin, `OpenAPI` routes, and the update guard.
454///
455/// This is called after vehicle data has been loaded. The `build_update_plugin` optional
456/// builder is consumed here; when `None` the runtime-update endpoints are simply not mounted.
457pub(crate) async fn setup_runtime_routes<SP, SL, UPB, CPB>(
458    config: Configuration,
459    vehicle_data: VehicleData<SP>,
460    ws: &ApplicationState,
461    build_update_plugin: Option<UPB>,
462    communication_plugin: CPB,
463    storage: Arc<LocalStorage>,
464) -> Result<CommunicationRuntime, AppError>
465where
466    SP: SecurityPlugin,
467    SL: SecurityPluginLoader,
468    UPB: UpdatePluginBuilder<SP>,
469    CPB: CommunicationPluginBuilder,
470{
471    let flash_files_path = config.flash_files_path.clone();
472    let flash_functional_class_default = config.flash_functional_class_default.clone();
473    let components_config = config.components.clone();
474    let mdd_decompress = config.flat_buf.mdd_decompress;
475
476    let runtime_update_config = config.runtime_update_config.clone();
477    let update_retry_after = Duration::from_secs(runtime_update_config.retry_after_seconds);
478    let communication_retry_after =
479        Duration::from_secs(config.communication.deferred_retry_after_seconds);
480    let post_update_mode = config.communication.post_update_mode.clone();
481
482    let lock_provider: Arc<cda_sovd::SovdLockStateProvider> = Arc::new(
483        cda_sovd::SovdLockStateProvider::new(Arc::clone(&vehicle_data.locks)),
484    );
485
486    let shutdown_signal = cda_interfaces::shutdown_signal(ws.shutdown_signal.clone());
487    let gateway = vehicle_data.diagnostic_gateway.clone();
488    let http_protections = HttpProtectionRegistry::new();
489
490    let (communication_runtime, communication_access) = build_communication_runtime_and_access(
491        ws,
492        vehicle_data.diagnostic_gateway.clone(),
493        communication_plugin,
494        config.communication.init_mode,
495        config.communication.variant_detection,
496        communication_retry_after,
497        &http_protections,
498    )
499    .await?;
500    let plugin = Arc::clone(&communication_runtime.plugin);
501
502    let components = crate::vehicle::finish_vehicle_components(
503        vehicle_data.prepared,
504        &config,
505        Arc::clone(&lock_provider)
506            as Arc<dyn cda_interfaces::request_authorization::RequestLockProvider>,
507        Arc::clone(&communication_access),
508    );
509    let uds_manager = ComponentSlot::new(components.uds_manager);
510    let file_managers = components.file_managers;
511
512    register_communication_hooks(&plugin, &uds_manager).await?;
513
514    let communication_disable: Arc<dyn DisableCommunication> =
515        Arc::new(CommunicationDisableView::new(Arc::clone(&plugin)));
516
517    // Routes are published only after the plugin, initializers, and narrow views are ready.
518    let vehicle_route_handle = cda_sovd::add_vehicle_routes::<_, _, SL>(
519        &ws.dynamic_router,
520        cda_sovd::VehicleConfig {
521            flash_files_path: config.flash_files_path.clone(),
522            flash_functional_class_default: config.flash_functional_class_default.clone(),
523            functional_group_config: config.functional_description.clone(),
524            components_config: config.components.clone(),
525        },
526        cda_sovd::VehicleResources {
527            ecu_uds: uds_manager.read().await.clone(),
528            file_managers,
529            locks: Arc::clone(&vehicle_data.locks),
530            communication_access: Arc::clone(&communication_access),
531        },
532    )
533    .await?;
534
535    activate_communication_per_init_mode(&plugin, config.communication.init_mode).await?;
536
537    let infra = CdaRuntime {
538        config: Arc::new(RwLock::new(config)),
539        dynamic_router: ws.dynamic_router.clone(),
540        vehicle_route_handle,
541        flash_files_path,
542        flash_functional_class_default,
543        components_config,
544        lock_provider: Arc::clone(&lock_provider),
545        shutdown_signal,
546        post_update_mode,
547        communication_access,
548        communication_disable,
549        http_protections,
550        update_retry_after,
551        uds_manager_replacer: uds_manager.replacer(),
552        gateway_replacer: gateway.replacer(),
553        health: vehicle_data.health_providers,
554        storage,
555        mdd_decompress,
556    };
557
558    setup_update_plugin::<SP, SL, _>(
559        ws,
560        build_update_plugin,
561        infra,
562        lock_provider,
563        runtime_update_config.upload_body_limit_bytes,
564        update_retry_after,
565    )
566    .await?;
567
568    Ok(communication_runtime)
569}
570
571#[cfg(test)]
572mod tests {
573    use std::sync::Arc;
574
575    use async_trait::async_trait;
576    use cda_interfaces::{
577        communication_control::{
578            CommControlError, CommunicationState, TransportControl, TransportState,
579        },
580        runtime_update_api::{
581            BulkDataCreatedList, BulkDataList, ExecutionMode, RuntimeFilesQuery,
582            RuntimeFilesUpdatePlugin, RuntimeUpdateError, UpdateExecution,
583        },
584    };
585    use cda_plugin_communication_management::{
586        lifecycle::build_communication_runtime, plugin::default::DefaultCommunicationPluginBuilder,
587    };
588    use cda_plugin_security::{DefaultSecurityPlugin, DefaultSecurityPluginData};
589
590    use super::*;
591    use crate::update::{UpdatePluginFn, update_plugin_fn};
592
593    // Minimal no-op plugin for type-checking.
594    struct NoOpPlugin;
595
596    #[async_trait::async_trait]
597    impl RuntimeFilesUpdatePlugin for NoOpPlugin {
598        async fn list_current(
599            &self,
600            _q: &RuntimeFilesQuery,
601        ) -> Result<BulkDataList, RuntimeUpdateError> {
602            Ok(BulkDataList::default())
603        }
604
605        async fn list_nextupdate(
606            &self,
607            _q: &RuntimeFilesQuery,
608        ) -> Result<BulkDataList, RuntimeUpdateError> {
609            Ok(BulkDataList::default())
610        }
611
612        async fn list_backup(
613            &self,
614            _q: &RuntimeFilesQuery,
615        ) -> Result<BulkDataList, RuntimeUpdateError> {
616            Ok(BulkDataList::default())
617        }
618
619        async fn upload(
620            &self,
621            _files: Vec<cda_interfaces::runtime_update_api::UploadFile>,
622        ) -> Result<BulkDataCreatedList, RuntimeUpdateError> {
623            Ok(BulkDataCreatedList::default())
624        }
625
626        async fn delete_nextupdate(&self) -> Result<Vec<String>, RuntimeUpdateError> {
627            Ok(vec![])
628        }
629
630        async fn delete_nextupdate_by_id(&self, _id: &str) -> Result<(), RuntimeUpdateError> {
631            Ok(())
632        }
633
634        async fn delete_backup(&self) -> Result<Vec<String>, RuntimeUpdateError> {
635            Ok(vec![])
636        }
637
638        async fn start_execution(
639            &self,
640            _mode: ExecutionMode,
641        ) -> Result<String, RuntimeUpdateError> {
642            Ok(String::new())
643        }
644
645        async fn list_executions(&self) -> Vec<UpdateExecution> {
646            vec![]
647        }
648
649        async fn get_execution_status(&self, _id: &str) -> Option<UpdateExecution> {
650            None
651        }
652    }
653
654    type TestSetup = Setup<DefaultSecurityPluginData, DefaultSecurityPlugin>;
655
656    struct NoopTransport;
657
658    #[async_trait]
659    impl TransportControl for NoopTransport {
660        async fn enable(&self) -> Result<(), CommControlError> {
661            Ok(())
662        }
663
664        async fn disable(&self) -> Result<(), CommControlError> {
665            Ok(())
666        }
667
668        async fn state(&self) -> TransportState {
669            TransportState::Disabled
670        }
671    }
672
[docs]673    /// [[ test~deferred-startup-policy, Startup activates communication only in Always mode, test ]]
674    #[tokio::test]
675    async fn startup_activation_follows_init_mode() {
676        for (mode, expected) in [
677            (CommunicationInitMode::Always, CommunicationState::Enabled),
678            (
679                CommunicationInitMode::OnDemand,
680                CommunicationState::Disabled,
681            ),
682            (
683                CommunicationInitMode::Disabled,
684                CommunicationState::Disabled,
685            ),
686        ] {
687            let runtime = build_communication_runtime(
688                DefaultCommunicationPluginBuilder,
689                Arc::new(NoopTransport),
690                mode,
691                VariantDetectionMode::Always,
692            )
693            .await
694            .unwrap();
695
696            activate_communication_per_init_mode(&runtime.plugin, mode)
697                .await
698                .unwrap();
699            assert_eq!(runtime.plugin.state(), expected, "{mode:?}");
700            runtime.plugin.shutdown().await;
701        }
702    }
703
704    #[test]
705    fn documented_public_api_type_checks() {
706        let _: Option<CdaRuntime<DefaultSecurityPluginData>> = None;
707    }
708
709    #[test]
710    fn new_has_no_preload_or_update_plugin() {
711        let s = TestSetup::new();
712        assert!(
713            s.pre_load.is_none(),
714            "fresh Setup must have no preload hook"
715        );
716        assert!(
717            s.build_update_plugin.is_none(),
718            "fresh Setup must have no update plugin"
719        );
720    }
721
722    #[test]
723    fn with_preload_stores_hook() {
724        let s = TestSetup::new().with_preload(|_router| async { Ok(()) });
725        assert!(
726            s.pre_load.is_some(),
727            "with_preload must store the provided hook"
728        );
729    }
730
731    #[test]
732    fn with_update_plugin_stores_builder() {
733        // Use `update_plugin_fn` as a convenient closure adapter.
734        let builder: UpdatePluginFn<_> =
735            update_plugin_fn(|_infra: CdaRuntime<DefaultSecurityPluginData>| async {
736                Ok(NoOpPlugin)
737            });
738
739        let s: Setup<DefaultSecurityPluginData, DefaultSecurityPlugin, _> =
740            TestSetup::new().with_update_plugin(builder);
741
742        assert!(
743            s.build_update_plugin.is_some(),
744            "with_update_plugin must store the provided builder"
745        );
746    }
747
748    #[test]
749    fn chaining_preload_then_plugin_retains_both() {
750        let builder: UpdatePluginFn<_> =
751            update_plugin_fn(|_infra: CdaRuntime<DefaultSecurityPluginData>| async {
752                Ok(NoOpPlugin)
753            });
754
755        let s = TestSetup::new()
756            .with_preload(|_| async { Ok(()) })
757            .with_update_plugin(builder);
758
759        assert!(s.pre_load.is_some(), "preload hook must survive chaining");
760        assert!(
761            s.build_update_plugin.is_some(),
762            "plugin builder must be stored after chaining"
763        );
764    }
765
766    #[test]
767    fn chaining_plugin_then_preload_retains_both() {
768        let builder: UpdatePluginFn<_> =
769            update_plugin_fn(|_infra: CdaRuntime<DefaultSecurityPluginData>| async {
770                Ok(NoOpPlugin)
771            });
772
773        let s = TestSetup::new()
774            .with_update_plugin(builder)
775            .with_preload(|_| async { Ok(()) });
776
777        assert!(s.pre_load.is_some(), "preload hook must survive chaining");
778        assert!(
779            s.build_update_plugin.is_some(),
780            "plugin builder must survive chaining"
781        );
782    }
783}