1/*
   2 * SPDX-FileCopyrightText: 2025 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 */
  13use std::time::Duration;
  14
  15use cda_interfaces::HashMap;
  16use http::{HeaderMap, Method, StatusCode};
  17use opensovd_cda_lib::config::configfile::Configuration;
  18use serde::de::DeserializeOwned;
  19use sovd_interfaces::components::ecu::modes::{self, dtcsetting};
  20
  21use crate::{
  22    sovd::{
  23        self, compute_security_key, get_ecu_component,
  24        locks::{self, create_lock, lock_operation},
  25        put_mode,
  26    },
  27    util::{
  28        TestingError,
  29        ecusim::{self},
  30        http::{
  31            QueryParams, auth_header, extract_field_from_json, response_to_json, response_to_t,
  32            send_cda_request,
  33        },
  34        runtime::{
  35            TestRuntime, restart_cda, setup_integration_test, skip_for_can, skip_for_doip,
  36            start_ecu_sim_for_mode, stop_ecu_sim,
  37        },
  38    },
  39};
  40
  41/// Reads the Identification DID from the ECU over its transport. Unlike the
  42/// component listing (served from the loaded MDD even when the ECU is dead),
  43/// this request only succeeds if the ECU actually answers on the bus, so it
  44/// proves end-to-end liveness.
  45async fn assert_ecu_answers_on_bus(runtime: &TestRuntime, ecu_endpoint: &str) {
  46    let auth = auth_header(&runtime.config, None)
  47        .await
  48        .expect("auth header should be obtainable");
  49    let response = send_cda_request(
  50        &runtime.config,
  51        &format!("{ecu_endpoint}/data/identification"),
  52        StatusCode::OK,
  53        Method::GET,
  54        None,
  55        Some(&auth),
  56        None,
  57    )
  58    .await
  59    .expect("live Identification read over the bus should succeed");
  60    let json = response_to_json(&response).expect("data response should be JSON");
  61    assert!(
  62        json.to_string().contains("Identification"),
  63        "data response should contain the Identification parameter: {json}"
  64    );
  65}
  66
  67/// This ECU is missing comm parameters and thus must be configured via the configuration file.
  68/// The test verifies that the ECU is reachable and reports the correct name and state.
  69#[tokio::test]
  70async fn test_tmcc3000_ecu_online() {
  71    let (runtime, _lock) = setup_integration_test(false).await.unwrap();
  72
  73    let json = get_ecu_component(
  74        &runtime.config,
  75        sovd::ECU_TMCC3000_ENDPOINT,
  76        StatusCode::OK,
  77        None,
  78    )
  79    .await
  80    .expect("TMCC3000 component should be reachable via SOVD API");
  81
  82    let name = json
  83        .get("name")
  84        .and_then(|v| v.as_str())
  85        .expect("Response should contain 'name' field");
  86    assert_eq!(
  87        name.to_lowercase(),
  88        "tmcc3000",
  89        "Component name should be tmcc3000"
  90    );
  91
  92    assert_ecu_answers_on_bus(runtime, sovd::ECU_TMCC3000_ENDPOINT).await;
  93}
  94
  95/// HOVR4000 uses a non-default protocol (`DMC_DoIP`) in its MDD. The global
  96/// protocol is `UDS_Ethernet_DoIP_DOBT`, so without a per-ECU protocol override
  97/// the CDA would fail to load this ECU.  The test verifies that the per-ECU
  98/// `protocol` config override works correctly.
  99#[tokio::test]
 100async fn test_hovr4000_per_ecu_protocol_override() {
 101    let (runtime, _lock) = setup_integration_test(false).await.unwrap();
 102
 103    let json = get_ecu_component(
 104        &runtime.config,
 105        sovd::ECU_HOVR4000_ENDPOINT,
 106        StatusCode::OK,
 107        None,
 108    )
 109    .await
 110    .expect("HOVR4000 component should be reachable when per-ECU protocol override is set");
 111
 112    let name = json
 113        .get("name")
 114        .and_then(|v| v.as_str())
 115        .expect("Response should contain 'name' field");
 116    assert_eq!(
 117        name.to_lowercase(),
 118        "hovr4000",
 119        "Component name should be hovr4000"
 120    );
 121
 122    assert_ecu_answers_on_bus(runtime, sovd::ECU_HOVR4000_ENDPOINT).await;
 123}
 124
 125/// JGWT5000 has a non-default protocol (`DMC_DoIP`) in its MDD but no per-ECU
 126/// protocol override.  With `ignore_protocol` enabled, `into_db_protocol` falls
 127/// back to the single DB protocol and com-param lookup matches by name alone.
 128#[tokio::test]
 129async fn test_jgwt5000_ignore_protocol_with_db_protocol() {
 130    let (runtime, _lock) = setup_integration_test(false).await.unwrap();
 131
 132    let json = get_ecu_component(
 133        &runtime.config,
 134        sovd::ECU_JGWT5000_ENDPOINT,
 135        StatusCode::OK,
 136        None,
 137    )
 138    .await
 139    .expect("JGWT5000 component should be reachable with ignore_protocol and no protocol override");
 140
 141    let name = json
 142        .get("name")
 143        .and_then(|v| v.as_str())
 144        .expect("Response should contain 'name' field");
 145    assert_eq!(
 146        name.to_lowercase(),
 147        "jgwt5000",
 148        "Component name should be jgwt5000"
 149    );
 150
 151    assert_ecu_answers_on_bus(runtime, sovd::ECU_JGWT5000_ENDPOINT).await;
 152}
 153
 154/// A CAN-only ECU must be usable purely from configuration: TMCC3000's MDD
 155/// carries no `DoIP` addressing and its CAN request/response IDs come
 156/// exclusively from `[[can.ecu_mappings]]` plus the per-ECU protocol
 157/// handling in the test config (in mixed mode additionally a transport
 158/// override pins it to CAN). The test asserts the ECU is actually served
 159/// over a CAN network address and answers a live read on the bus.
 160#[tokio::test]
 161async fn test_can_only_ecu_from_configuration() {
 162    if skip_for_doip(
 163        "test_can_only_ecu_from_configuration",
 164        "needs the CAN transport (pure-CAN or mixed mode)",
 165    ) {
 166        return;
 167    }
 168    let (runtime, _lock) = setup_integration_test(false).await.unwrap();
 169
 170    // Live read proves the ECU answers on the bus at all.
 171    assert_ecu_answers_on_bus(runtime, sovd::ECU_TMCC3000_ENDPOINT).await;
 172
 173    // The network structure must serve TMCC3000 behind a CAN network address
 174    // (can:// scheme) carrying the configured request/response CAN IDs.
 175    let response = send_cda_request(
 176        &runtime.config,
 177        "apps/sovd2uds/data/networkstructure",
 178        StatusCode::OK,
 179        Method::GET,
 180        None,
 181        None,
 182        None,
 183    )
 184    .await
 185    .expect("network structure should be readable");
 186    let json = response_to_json(&response).expect("network structure should be JSON");
 187    let gateways: Vec<&serde_json::Value> = json
 188        .get("data")
 189        .and_then(|d| d.as_array())
 190        .map(|structures| {
 191            structures
 192                .iter()
 193                .filter_map(|ns| ns.get("Gateways").and_then(|g| g.as_array()))
 194                .flatten()
 195                .collect()
 196        })
 197        .unwrap_or_default();
 198    let tmcc3000_gateway = gateways
 199        .iter()
 200        .find(|gw| {
 201            gw.get("Ecus")
 202                .and_then(|ecus| ecus.as_array())
 203                .is_some_and(|ecus| {
 204                    ecus.iter().any(|ecu| {
 205                        ecu.get("Qualifier")
 206                            .and_then(|q| q.as_str())
 207                            .is_some_and(|q| q.eq_ignore_ascii_case("tmcc3000"))
 208                    })
 209                })
 210        })
 211        .unwrap_or_else(|| panic!("TMCC3000 missing from network structure: {json}"));
 212    let network_address = tmcc3000_gateway
 213        .get("NetworkAddress")
 214        .and_then(|a| a.as_str())
 215        .expect("gateway should have a network address");
 216    assert!(
 217        network_address.starts_with("can://"),
 218        "TMCC3000 should be served over CAN, got network address {network_address}"
 219    );
 220    assert!(
 221        network_address.contains("0x730") && network_address.contains("0x738"),
 222        "CAN address should carry the configured request/response IDs: {network_address}"
 223    );
 224}
 225
 226#[allow(clippy::too_many_lines, reason = "Makes sense to keep test together")]
 227#[tokio::test]
 228async fn test_ecu_session_switching() {
 229    // TODO(can): SecurityAccess seed/key/lock sequencing is not yet reliable
 230    // over the CAN transport. Re-enable once the CAN session/security path
 231    // is hardened, see #444
 232    if skip_for_can(
 233        "test_ecu_session_switching",
 234        "SecurityAccess sequencing not yet supported over CAN",
 235    ) {
 236        return;
 237    }
 238    let (runtime, _lock) = setup_integration_test(true).await.unwrap();
 239    let auth = auth_header(&runtime.config, None).await.unwrap();
 240    let ecu_endpoint = sovd::ECU_FLXC1000_ENDPOINT;
 241
 242    // We have no lock yet, thus the CDA should reject the request to send the key.
 243    send_key(
 244        "Level_5".to_owned(),
 245        "0x42".to_owned(),
 246        &runtime.config,
 247        &auth,
 248        ecu_endpoint,
 249        StatusCode::FORBIDDEN,
 250    )
 251    .await
 252    .unwrap();
 253
 254    let expiration_timeout = Duration::from_secs(60);
 255    let ecu_lock = create_lock(
 256        expiration_timeout,
 257        locks::ECU_ENDPOINT,
 258        StatusCode::CREATED,
 259        &runtime.config,
 260        &auth,
 261    )
 262    .await;
 263    let lock_id =
 264        extract_field_from_json::<String>(&response_to_json(&ecu_lock).unwrap(), "id").unwrap();
 265
 266    // Lock the ECU
 267    lock_operation(
 268        locks::ECU_ENDPOINT,
 269        Some(&lock_id),
 270        &runtime.config,
 271        &auth,
 272        StatusCode::OK,
 273        Method::GET,
 274    )
 275    .await;
 276
 277    force_variant_detection(&runtime.config, &auth, ecu_endpoint)
 278        .await
 279        .unwrap();
 280
 281    let ecu = ecu_status(&runtime.config, &auth, ecu_endpoint)
 282        .await
 283        .unwrap();
 284    assert!(ecu.name.eq_ignore_ascii_case("flxc1000"));
 285    assert_eq!(ecu.variant.name, "FLXC1000_App_0101".to_string());
 286
 287    switch_session(
 288        "this status does not exist",
 289        &runtime.config,
 290        &auth,
 291        ecu_endpoint,
 292        StatusCode::NOT_FOUND,
 293    )
 294    .await
 295    .unwrap();
 296
 297    // Get the active diagnostic session using the Configuration GET method.
 298    let get_config_result = get_configurations(
 299        &runtime.config,
 300        &auth,
 301        ecu_endpoint,
 302        "activediagnosticsessiondataidentifier",
 303    )
 304    .await
 305    .unwrap();
 306
 307    assert_eq!(
 308        get_config_result.id,
 309        "activediagnosticsessiondataidentifier"
 310    );
 311    let session_type = get_config_result
 312        .data
 313        .get("EcuSessionType")
 314        .and_then(|v| v.as_str())
 315        .expect("Missing or invalid EcuSessionType");
 316    assert_eq!(session_type, "Default");
 317
 318    let switch_session_result = switch_session(
 319        "extended",
 320        &runtime.config,
 321        &auth,
 322        ecu_endpoint,
 323        StatusCode::OK,
 324    )
 325    .await
 326    .unwrap()
 327    .unwrap();
 328    assert_eq!(switch_session_result.value.to_lowercase(), "extended");
 329    let session_result = session(&runtime.config, &auth, ecu_endpoint).await.unwrap();
 330    assert_eq!(
 331        session_result.value.map(|s| s.to_lowercase()),
 332        Some("extended".to_owned())
 333    );
 334    assert_eq!(session_result.name, Some("Diagnostic session".to_owned()));
 335
 336    // After switching to extended session, fetch again using configuraion GET and verify.
 337    let get_config_result = get_configurations(
 338        &runtime.config,
 339        &auth,
 340        ecu_endpoint,
 341        "activediagnosticsessiondataidentifier",
 342    )
 343    .await
 344    .unwrap();
 345
 346    assert_eq!(
 347        get_config_result.id,
 348        "activediagnosticsessiondataidentifier"
 349    );
 350    let session_type = get_config_result
 351        .data
 352        .get("EcuSessionType")
 353        .and_then(|v| v.as_str())
 354        .expect("Missing or invalid EcuSessionType");
 355    assert_eq!(session_type, "Extended");
 356
 357    // Reset the ECU using the reset service and verify the session goes back to default
 358    reset_ecu(
 359        "hardreset",
 360        &runtime.config,
 361        &auth,
 362        ecu_endpoint,
 363        StatusCode::NO_CONTENT,
 364    )
 365    .await
 366    .unwrap();
 367
 368    let session_result_after_reset = session(&runtime.config, &auth, ecu_endpoint).await.unwrap();
 369    assert_eq!(
 370        session_result_after_reset.value.map(|s| s.to_lowercase()),
 371        Some("default".to_owned()),
 372        "Session should be back to default after hard reset"
 373    );
 374
 375    // Switch back to extended session so the remaining test steps work
 376    let switch_back_result = switch_session(
 377        "extended",
 378        &runtime.config,
 379        &auth,
 380        ecu_endpoint,
 381        StatusCode::OK,
 382    )
 383    .await
 384    .unwrap()
 385    .unwrap();
 386    assert_eq!(switch_back_result.value.to_lowercase(), "extended");
 387
 388    // switch ECU sim state to BOOT
 389    ecusim::switch_variant(&runtime.ecu_sim, "FLXC1000", "BOOT")
 390        .await
 391        .unwrap();
 392    force_variant_detection(&runtime.config, &auth, ecu_endpoint)
 393        .await
 394        .unwrap();
 395    let ecu = ecu_status(&runtime.config, &auth, ecu_endpoint)
 396        .await
 397        .unwrap();
 398    assert_eq!(ecu.variant.name, "FLXC1000_Boot_Variant".to_string());
 399
 400    let seed_response = request_seed(
 401        "Level_5_RequestSeed".to_owned(),
 402        &runtime.config,
 403        &auth,
 404        ecu_endpoint,
 405    )
 406    .await
 407    .unwrap()
 408    .unwrap();
 409
 410    // Key is too short
 411    send_key(
 412        "Level_5".to_owned(),
 413        "0x42".to_owned(),
 414        &runtime.config,
 415        &auth,
 416        ecu_endpoint,
 417        StatusCode::BAD_GATEWAY,
 418    )
 419    .await
 420    .unwrap();
 421
 422    send_key(
 423        "Level_5".to_owned(),
 424        seed_response.seed.request_seed.clone(),
 425        &runtime.config,
 426        &auth,
 427        ecu_endpoint,
 428        StatusCode::BAD_GATEWAY,
 429    )
 430    .await
 431    .unwrap();
 432
 433    let key = compute_security_key(&seed_response.seed.request_seed);
 434
 435    send_key(
 436        "Level_5".to_owned(),
 437        key,
 438        &runtime.config,
 439        &auth,
 440        ecu_endpoint,
 441        StatusCode::OK,
 442    )
 443    .await
 444    .unwrap();
 445    let security_result = security(&runtime.config, &auth, ecu_endpoint)
 446        .await
 447        .unwrap();
 448    assert_eq!(security_result.value, Some("Level_5".to_owned()));
 449    assert_eq!(security_result.name, Some("Security access".to_owned()));
 450
 451    // Delete the ECU lock
 452    lock_operation(
 453        locks::ECU_ENDPOINT,
 454        Some(&lock_id),
 455        &runtime.config,
 456        &auth,
 457        StatusCode::NO_CONTENT,
 458        Method::DELETE,
 459    )
 460    .await;
 461}
 462
 463#[tokio::test]
 464async fn test_variant_detection_duplicates() {
 465    // DoIP-only: relies on spontaneous VAM announcements and restarts the sim's
 466    // DoIP entities (which has no CAN-hub equivalent), so it cannot run over the
 467    // CAN transport.
 468    if skip_for_can(
 469        "test_variant_detection_duplicates",
 470        "depends on DoIP VAM announcements and sim restart",
 471    ) {
 472        return;
 473    }
 474    let (runtime, _lock) = setup_integration_test(true).await.unwrap();
 475    let auth = auth_header(&runtime.config, None).await.unwrap();
 476
 477    // Switch variant, and check if the NG variant is now online.
 478    ecusim::switch_variant(&runtime.ecu_sim, "FLXC1000", "APPLICATION")
 479        .await
 480        .unwrap();
 481    force_variant_detection(&runtime.config, &auth, sovd::ECU_FLXC1000_ENDPOINT)
 482        .await
 483        .unwrap();
 484    let ecu = ecu_status(&runtime.config, &auth, sovd::ECU_FLXC1000_ENDPOINT)
 485        .await
 486        .unwrap();
 487    assert_eq!(
 488        ecu.variant.state,
 489        sovd_interfaces::components::ecu::State::Online
 490    );
 491    assert_eq!(ecu.variant.logical_address, "0x1000");
 492
 493    // Switch variant, and check if the NG variant is now online.
 494    ecusim::switch_variant(&runtime.ecu_sim, "FLXC1000", "APPLICATION2")
 495        .await
 496        .unwrap();
 497    force_variant_detection(&runtime.config, &auth, sovd::ECU_FLXC1000_ENDPOINT)
 498        .await
 499        .unwrap();
 500
 501    validate_ecu_state(
 502        runtime,
 503        &auth,
 504        sovd::ECU_FLXC1000_ENDPOINT,
 505        sovd_interfaces::components::ecu::State::Duplicate,
 506    )
 507    .await;
 508
 509    validate_ecu_state(
 510        runtime,
 511        &auth,
 512        sovd::ECU_FLXCNG1000_ENDPOINT,
 513        sovd_interfaces::components::ecu::State::Online,
 514    )
 515    .await;
 516
 517    // No variant associated with APPLICATION3, check if both ECUs are marked as NoVariantDetected
 518    ecusim::switch_variant(&runtime.ecu_sim, "FLXC1000", "APPLICATION3")
 519        .await
 520        .unwrap();
 521    force_variant_detection(&runtime.config, &auth, sovd::ECU_FLXC1000_ENDPOINT)
 522        .await
 523        .unwrap();
 524    validate_ecu_state(
 525        runtime,
 526        &auth,
 527        sovd::ECU_FLXC1000_ENDPOINT,
 528        sovd_interfaces::components::ecu::State::NoVariantDetected,
 529    )
 530    .await;
 531    validate_ecu_state(
 532        runtime,
 533        &auth,
 534        sovd::ECU_FLXCNG1000_ENDPOINT,
 535        sovd_interfaces::components::ecu::State::NoVariantDetected,
 536    )
 537    .await;
 538
 539    // Stop sim and check if ECUs are marked as disconnected after variant detection
 540    stop_ecu_sim().await.unwrap();
 541    force_variant_detection(&runtime.config, &auth, sovd::ECU_FLXCNG1000_ENDPOINT)
 542        .await
 543        .unwrap();
 544
 545    validate_ecu_state(
 546        runtime,
 547        &auth,
 548        sovd::ECU_FLXC1000_ENDPOINT,
 549        sovd_interfaces::components::ecu::State::Disconnected,
 550    )
 551    .await;
 552    validate_ecu_state(
 553        runtime,
 554        &auth,
 555        sovd::ECU_FLXCNG1000_ENDPOINT,
 556        sovd_interfaces::components::ecu::State::Disconnected,
 557    )
 558    .await;
 559
 560    // restart CDA while sim is offline and check if ECUs are marked as offline
 561    restart_cda(&runtime.config).await.unwrap();
 562    validate_ecu_state(
 563        runtime,
 564        &auth,
 565        sovd::ECU_FLXC1000_ENDPOINT,
 566        sovd_interfaces::components::ecu::State::Offline,
 567    )
 568    .await;
 569    validate_ecu_state(
 570        runtime,
 571        &auth,
 572        sovd::ECU_FLXCNG1000_ENDPOINT,
 573        sovd_interfaces::components::ecu::State::Offline,
 574    )
 575    .await;
 576
 577    // restart sim and wait for ECUs to come online,
 578    // status should be detected without manual variant detection
 579    start_ecu_sim_for_mode(&runtime.ecu_sim).await.unwrap();
 580
 581    // wait in loop, to check if the CDA receives the spontaneous VAM when is online
 582    for attempt in 0..=5 {
 583        let status = ecu_status(&runtime.config, &auth, sovd::ECU_FLXC1000_ENDPOINT)
 584            .await
 585            .expect("failed to get ecu status");
 586
 587        if status.variant.state == sovd_interfaces::components::ecu::State::Online {
 588            break;
 589        }
 590
 591        assert!(
 592            attempt < 5,
 593            "ECU did not come online in time, status {status:?}"
 594        );
 595        cda_interfaces::util::tokio_ext::sleep_for(Duration::from_secs(1)).await;
 596    }
 597
 598    validate_ecu_state(
 599        runtime,
 600        &auth,
 601        sovd::ECU_FLXCNG1000_ENDPOINT,
 602        sovd_interfaces::components::ecu::State::Duplicate,
 603    )
 604    .await;
 605}
 606
 607#[tokio::test]
 608#[allow(clippy::too_many_lines, reason = "Keep the test together")]
 609async fn test_communication_control() {
 610    let (runtime, _lock) = setup_integration_test(true).await.unwrap();
 611    let auth = auth_header(&runtime.config, None).await.unwrap();
 612    let ecu_endpoint = sovd::ECU_FLXC1000_ENDPOINT;
 613
 614    // Without lock, the CDA should reject the request
 615    set_comm_control(
 616        "EnableRxAndEnableTx",
 617        None,
 618        &runtime.config,
 619        &auth,
 620        ecu_endpoint,
 621        StatusCode::FORBIDDEN,
 622    )
 623    .await
 624    .unwrap();
 625
 626    // Create and acquire lock
 627    let expiration_timeout = Duration::from_secs(60);
 628    let ecu_lock = create_lock(
 629        expiration_timeout,
 630        locks::ECU_ENDPOINT,
 631        StatusCode::CREATED,
 632        &runtime.config,
 633        &auth,
 634    )
 635    .await;
 636    let lock_id =
 637        extract_field_from_json::<String>(&response_to_json(&ecu_lock).unwrap(), "id").unwrap();
 638
 639    // Sending an invalid value should return BAD_REQUEST with possible values
 640    sovd::validate_invalid_parameter_error(
 641        &runtime.config,
 642        &auth,
 643        ecu_endpoint,
 644        "commctrl",
 645        modes::commctrl::put::Request {
 646            value: "invalid-value".to_owned(),
 647            parameters: None,
 648        },
 649        &[
 650            "enablerxandenabletx",
 651            "enablerxanddisabletx",
 652            "disablerxandenabletx",
 653            "disablerxanddisabletx",
 654            "enablerxanddisabletxwithenhancedaddressinformation",
 655            "enablerxandtxwithenhancedaddressinformation",
 656            "temporalsync",
 657        ],
 658    )
 659    .await
 660    .unwrap();
 661
 662    let enable_rx_and_enable_tx = "enablerxandenabletx";
 663    let result = set_comm_control(
 664        "EnableRxAndEnableTx",
 665        None,
 666        &runtime.config,
 667        &auth,
 668        ecu_endpoint,
 669        StatusCode::OK,
 670    )
 671    .await
 672    .unwrap()
 673    .unwrap();
 674    assert_eq!(result.value, "EnableRxAndEnableTx");
 675
 676    let current_state = get_comm_control(&runtime.config, &auth, ecu_endpoint)
 677        .await
 678        .unwrap();
 679    assert_eq!(
 680        current_state.value.as_ref().map(|s| s.to_lowercase()),
 681        Some(enable_rx_and_enable_tx.to_owned())
 682    );
 683
 684    let enable_rx_and_disable_tx = "enablerxanddisabletx";
 685    let result = set_comm_control(
 686        "EnableRxAndDisableTx",
 687        None,
 688        &runtime.config,
 689        &auth,
 690        ecu_endpoint,
 691        StatusCode::OK,
 692    )
 693    .await
 694    .unwrap()
 695    .unwrap();
 696    assert_eq!(result.value, "EnableRxAndDisableTx");
 697
 698    let current_state = get_comm_control(&runtime.config, &auth, ecu_endpoint)
 699        .await
 700        .unwrap();
 701    assert_eq!(
 702        current_state.value.as_ref().map(|s| s.to_lowercase()),
 703        Some(enable_rx_and_disable_tx.to_owned())
 704    );
 705
 706    let disable_rx_and_enable_tx = "disablerxandenabletx";
 707    let result = set_comm_control(
 708        "DisableRxAndEnableTx",
 709        None,
 710        &runtime.config,
 711        &auth,
 712        ecu_endpoint,
 713        StatusCode::OK,
 714    )
 715    .await
 716    .unwrap()
 717    .unwrap();
 718    assert_eq!(result.value, "DisableRxAndEnableTx");
 719
 720    let current_state = get_comm_control(&runtime.config, &auth, ecu_endpoint)
 721        .await
 722        .unwrap();
 723    assert_eq!(
 724        current_state.value.as_ref().map(|s| s.to_lowercase()),
 725        Some(disable_rx_and_enable_tx.to_owned())
 726    );
 727
 728    let disable_rx_and_disable_tx = "disablerxanddisabletx";
 729    let result = set_comm_control(
 730        "DisableRxAndDisableTx",
 731        None,
 732        &runtime.config,
 733        &auth,
 734        ecu_endpoint,
 735        StatusCode::OK,
 736    )
 737    .await
 738    .unwrap()
 739    .unwrap();
 740    assert_eq!(result.value, "DisableRxAndDisableTx");
 741
 742    let current_state = get_comm_control(&runtime.config, &auth, ecu_endpoint)
 743        .await
 744        .unwrap();
 745    assert_eq!(
 746        current_state.value.as_ref().map(|s| s.to_lowercase()),
 747        Some(disable_rx_and_disable_tx.to_owned())
 748    );
 749
 750    let enable_rx_and_disable_tx_with_enhanced =
 751        "enablerxanddisabletxwithenhancedaddressinformation";
 752    let result = set_comm_control(
 753        "EnableRxAndDisableTxWithEnhancedAddressInformation",
 754        None,
 755        &runtime.config,
 756        &auth,
 757        ecu_endpoint,
 758        StatusCode::OK,
 759    )
 760    .await
 761    .unwrap()
 762    .unwrap();
 763    assert_eq!(
 764        result.value,
 765        "EnableRxAndDisableTxWithEnhancedAddressInformation"
 766    );
 767
 768    let current_state = get_comm_control(&runtime.config, &auth, ecu_endpoint)
 769        .await
 770        .unwrap();
 771    assert_eq!(
 772        current_state.value.as_ref().map(|s| s.to_lowercase()),
 773        Some(enable_rx_and_disable_tx_with_enhanced.to_owned())
 774    );
 775
 776    let enable_rx_and_tx_with_enhanced = "enablerxandtxwithenhancedaddressinformation";
 777    let result = set_comm_control(
 778        "EnableRxAndTxWithEnhancedAddressInformation",
 779        None,
 780        &runtime.config,
 781        &auth,
 782        ecu_endpoint,
 783        StatusCode::OK,
 784    )
 785    .await
 786    .unwrap()
 787    .unwrap();
 788    assert_eq!(result.value, "EnableRxAndTxWithEnhancedAddressInformation");
 789
 790    let current_state = get_comm_control(&runtime.config, &auth, ecu_endpoint)
 791        .await
 792        .unwrap();
 793    assert_eq!(
 794        current_state.value.as_ref().map(|s| s.to_lowercase()),
 795        Some(enable_rx_and_tx_with_enhanced.to_owned())
 796    );
 797
 798    // VendorSpecific (custom TemporalSync 0x88)
 799    let temporal_era_id: i32 = -1_373_112_000;
 800    let mut parameters = cda_interfaces::HashMap::default();
 801    parameters.insert(
 802        "temporalEraId".to_string(),
 803        serde_json::json!(temporal_era_id),
 804    );
 805
 806    let temporal_sync = "temporalsync";
 807    let result = set_comm_control(
 808        "TemporalSync",
 809        Some(parameters),
 810        &runtime.config,
 811        &auth,
 812        ecu_endpoint,
 813        StatusCode::OK,
 814    )
 815    .await
 816    .unwrap()
 817    .unwrap();
 818    assert_eq!(result.value, "TemporalSync");
 819
 820    let current_state = get_comm_control(&runtime.config, &auth, ecu_endpoint)
 821        .await
 822        .unwrap();
 823    assert_eq!(
 824        current_state.value.as_ref().map(|s| s.to_lowercase()),
 825        Some(temporal_sync.to_owned())
 826    );
 827
 828    // Validate that ECU sim received and stored the temporalEraId
 829    let ecu_state = ecusim::get_ecu_state(&runtime.ecu_sim, "flxc1000")
 830        .await
 831        .expect("Failed to get ECU sim state");
 832    assert_eq!(
 833        ecu_state.temporal_era_id,
 834        Some(temporal_era_id),
 835        "ECU sim did not store the correct temporalEraId, state={ecu_state:#?}",
 836    );
 837    assert_eq!(
 838        ecu_state.communication_control_type,
 839        Some(ecusim::CommunicationControlType::TemporalSync)
 840    );
 841
 842    // Delete the ECU lock
 843    lock_operation(
 844        locks::ECU_ENDPOINT,
 845        Some(&lock_id),
 846        &runtime.config,
 847        &auth,
 848        StatusCode::NO_CONTENT,
 849        Method::DELETE,
 850    )
 851    .await;
 852
 853    // After deleting lock, we should not be able to set comm control
 854    set_comm_control(
 855        "EnableRxAndEnableTx",
 856        None,
 857        &runtime.config,
 858        &auth,
 859        ecu_endpoint,
 860        StatusCode::FORBIDDEN,
 861    )
 862    .await
 863    .unwrap();
 864}
 865
 866#[tokio::test]
 867async fn test_boot_variant_service_inheritance() {
 868    let (runtime, _lock) = setup_integration_test(true).await.unwrap();
 869    let auth = auth_header(&runtime.config, None).await.unwrap();
 870    let ecu_endpoint = sovd::ECU_FLXC1000_ENDPOINT;
 871
 872    // Switch ECU sim to BOOT variant
 873    ecusim::switch_variant(&runtime.ecu_sim, "FLXC1000", "BOOT")
 874        .await
 875        .unwrap();
 876    force_variant_detection(&runtime.config, &auth, ecu_endpoint)
 877        .await
 878        .unwrap();
 879
 880    let ecu = ecu_status(&runtime.config, &auth, ecu_endpoint)
 881        .await
 882        .unwrap();
 883    assert_eq!(ecu.variant.name, "FLXC1000_Boot_Variant".to_string());
 884
 885    let data_services = get_data_services(&runtime.config, &auth, ecu_endpoint)
 886        .await
 887        .unwrap();
 888    let service_ids: Vec<_> = data_services
 889        .items
 890        .iter()
 891        .map(|item| item.id.to_lowercase())
 892        .collect();
 893
 894    // Vindataidentifier is inherited and should be present in boot.
 895    assert!(
 896        service_ids.contains(&"vindataidentifier".to_owned()),
 897        "VIN service should be inherited from base variant, service ids {}",
 898        service_ids.join(", ")
 899    );
 900
 901    // reset ecu-sim variant
 902    ecusim::switch_variant(&runtime.ecu_sim, "FLXC1000", "APPLICATION")
 903        .await
 904        .unwrap();
 905
 906    // As long as test_ecu_session_switching also works we know that services
 907    // specific to the boot variant are still looked up correct, otherwise we cannot find
 908    // RequestSeed and SendKey services, no need to test this again here.
 909}
 910
 911#[tokio::test]
 912async fn test_ecu_session_reset_on_lock_reacquire() {
 913    // TODO(can): session expiry depends on TesterPresent keepalive cadence,
 914    // which is not yet reliable over the CAN transport (per-transaction
 915    // sockets + busy-poll dispatcher are too slow), see #444
 916    if skip_for_can(
 917        "test_ecu_session_reset_on_lock_reacquire",
 918        "session-expiry keepalive timing not yet reliable over CAN",
 919    ) {
 920        return;
 921    }
 922    let (runtime, _lock) = setup_integration_test(true).await.unwrap();
 923    let auth = auth_header(&runtime.config, None).await.unwrap();
 924    let ecu_endpoint = sovd::ECU_FLXC1000_ENDPOINT;
 925
 926    // Create and acquire lock with 30s timeout
 927    let lock_expiration_timeout = Duration::from_secs(30);
 928    let ecu_lock = create_lock(
 929        lock_expiration_timeout,
 930        locks::ECU_ENDPOINT,
 931        StatusCode::CREATED,
 932        &runtime.config,
 933        &auth,
 934    )
 935    .await;
 936    let lock_id =
 937        extract_field_from_json::<String>(&response_to_json(&ecu_lock).unwrap(), "id").unwrap();
 938
 939    // Set session with 2s expiry
 940    let session_expiration = 2u64;
 941    let switch_session_result: modes::security_and_session::put::Response<String> = put_mode(
 942        &runtime.config,
 943        &auth,
 944        ecu_endpoint,
 945        "session",
 946        modes::security_and_session::put::Request {
 947            value: "extended".to_owned(),
 948            mode_expiration: Some(session_expiration),
 949            key: None,
 950        },
 951        StatusCode::OK,
 952    )
 953    .await
 954    .unwrap()
 955    .unwrap();
 956    assert_eq!(switch_session_result.value.to_lowercase(), "extended");
 957
 958    // Verify ECU sim is in extended session
 959    let ecu_state = ecusim::get_ecu_state(&runtime.ecu_sim, "flxc1000")
 960        .await
 961        .expect("Failed to get ECU sim state");
 962    assert_eq!(
 963        ecu_state.session_state,
 964        Some(ecusim::SessionState::Extended),
 965        "ECU sim should be in Extended session"
 966    );
 967
 968    // Wait for the session to expire
 969    cda_interfaces::util::tokio_ext::sleep_for(Duration::from_secs(session_expiration + 1)).await;
 970
 971    // Check if the sim is back to default
 972    let ecu_state_after_expiry = ecusim::get_ecu_state(&runtime.ecu_sim, "flxc1000")
 973        .await
 974        .expect("Failed to get ECU sim state after session expiry");
 975
 976    assert_eq!(
 977        ecu_state_after_expiry.session_state,
 978        Some(ecusim::SessionState::Default),
 979        "ECU sim should be back to Default session after session expiry"
 980    );
 981
 982    // Also verify through CDA API
 983    let session_result_after = session(&runtime.config, &auth, ecu_endpoint).await.unwrap();
 984    assert_eq!(
 985        session_result_after.value.map(|s| s.to_lowercase()),
 986        Some("default".to_owned())
 987    );
 988
 989    // Delete the lock
 990    lock_operation(
 991        locks::ECU_ENDPOINT,
 992        Some(&lock_id),
 993        &runtime.config,
 994        &auth,
 995        StatusCode::NO_CONTENT,
 996        Method::DELETE,
 997    )
 998    .await;
 999}
1000
[docs]1001/// [[ itest~sovd-api-component-sdgsd, ECU-level SDG retrieval, itest ]]
1002#[tokio::test]
1003async fn test_ecu_sdg_retrieval() {
1004    let (runtime, _lock) = setup_integration_test(true).await.unwrap();
1005    let ecu_endpoint = sovd::ECU_FLXC1000_ENDPOINT;
1006
1007    // Retrieve sdgs and verify contents
1008    let params = QueryParams(HashMap::from_iter([(
1009        "x-sovd2uds-includesdgs".to_string(),
1010        "true".to_string(),
1011    )]));
1012    let data = get_ecu_component(&runtime.config, ecu_endpoint, StatusCode::OK, Some(&params))
1013        .await
1014        .unwrap();
1015
1016    let d = data
1017        .get("data")
1018        .unwrap()
1019        .as_str()
1020        .expect("should contain data");
1021    assert_eq!(
1022        d,
1023        "http://localhost:20002/vehicle/v15/components/flxc1000/data"
1024    );
1025
1026    let operations = data
1027        .get("operations")
1028        .unwrap()
1029        .as_str()
1030        .expect("should contain operations");
1031    assert_eq!(
1032        operations,
1033        "http://localhost:20002/vehicle/v15/components/flxc1000/operations"
1034    );
1035
1036    let configurations = data
1037        .get("configurations")
1038        .unwrap()
1039        .as_str()
1040        .expect("should contain configurations");
1041    assert_eq!(
1042        configurations,
1043        "http://localhost:20002/vehicle/v15/components/flxc1000/configurations"
1044    );
1045
1046    let modes = data
1047        .get("modes")
1048        .unwrap()
1049        .as_str()
1050        .expect("should contain modes");
1051    assert_eq!(
1052        modes,
1053        "http://localhost:20002/vehicle/v15/components/flxc1000/modes"
1054    );
1055
1056    let locks = data
1057        .get("locks")
1058        .unwrap()
1059        .as_str()
1060        .expect("should contain locks");
1061    assert_eq!(
1062        locks,
1063        "http://localhost:20002/vehicle/v15/components/flxc1000/locks"
1064    );
1065
1066    let faults = data
1067        .get("faults")
1068        .unwrap()
1069        .as_str()
1070        .expect("should contain faults");
1071    assert_eq!(
1072        faults,
1073        "http://localhost:20002/vehicle/v15/components/flxc1000/faults"
1074    );
1075
1076    let sdgs = data
1077        .get("sdgs")
1078        .unwrap()
1079        .as_array()
1080        .expect("sdgs should be an array");
1081    assert_eq!(sdgs.len(), 1);
1082
1083    let sdg = &sdgs.first().unwrap();
1084    assert_eq!(sdg.get("caption").unwrap().as_str(), Some("default_sdg"));
1085    assert_eq!(sdg.get("si").unwrap().as_str(), Some("default"));
1086
1087    let inner = sdg
1088        .get("sdgs")
1089        .unwrap()
1090        .as_array()
1091        .expect("nested sdgs should be an array");
1092    assert_eq!(inner.len(), 1);
1093
1094    let sd = &inner.first().unwrap();
1095    assert_eq!(
1096        sd.get("si").unwrap().as_str(),
1097        Some("power_requirement_max")
1098    );
1099    assert_eq!(sd.get("value").unwrap().as_str(), Some("1.21GW"));
1100}
1101
[docs]1102/// [[ itest~sovd-api-component-alias-sdgsd, ECU-level SDG retrieval (alias param), itest ]]
1103#[tokio::test]
1104async fn test_ecu_sdg_retrieval_alias() {
1105    let (runtime, _lock) = setup_integration_test(true).await.unwrap();
1106    let ecu_endpoint = sovd::ECU_FLXC1000_ENDPOINT;
1107
1108    // Retrieve sdgs and verify contents
1109    let params = QueryParams(HashMap::from_iter([(
1110        "x-include-sdgs".to_string(),
1111        "true".to_string(),
1112    )]));
1113    let data = get_ecu_component(&runtime.config, ecu_endpoint, StatusCode::OK, Some(&params))
1114        .await
1115        .unwrap();
1116
1117    let sdgs = data
1118        .get("sdgs")
1119        .unwrap()
1120        .as_array()
1121        .expect("sdgs should be an array");
1122    assert_eq!(sdgs.len(), 1);
1123
1124    let sdg = &sdgs.first().unwrap();
1125    assert_eq!(sdg.get("caption").unwrap().as_str(), Some("default_sdg"));
1126    assert_eq!(sdg.get("si").unwrap().as_str(), Some("default"));
1127
1128    let inner = sdg
1129        .get("sdgs")
1130        .unwrap()
1131        .as_array()
1132        .expect("nested sdgs should be an array");
1133    assert_eq!(inner.len(), 1);
1134
1135    let sd = &inner.first().unwrap();
1136    assert_eq!(
1137        sd.get("si").unwrap().as_str(),
1138        Some("power_requirement_max")
1139    );
1140    assert_eq!(sd.get("value").unwrap().as_str(), Some("1.21GW"));
1141}
1142
[docs]1143/// [[ itest~sovd-api-component-data-sdgsd, Data-level SDG retrieval, itest ]]
1144#[tokio::test]
1145async fn test_data_sdg_retrieval() {
1146    let (runtime, _lock) = setup_integration_test(true).await.unwrap();
1147    let auth = auth_header(&runtime.config, None).await.unwrap();
1148
1149    let params = QueryParams(HashMap::from_iter([(
1150        "x-sovd2uds-includesdgs".to_string(),
1151        "true".to_string(),
1152    )]));
1153    let response = send_cda_request(
1154        &runtime.config,
1155        &format!(
1156            "{}/data/FluxCapacitorPowerConsumption",
1157            sovd::ECU_FLXC1000_ENDPOINT
1158        ),
1159        StatusCode::OK,
1160        Method::GET,
1161        None,
1162        Some(&auth),
1163        Some(&params),
1164    )
1165    .await
1166    .expect("Failed to get data SDGs");
1167
1168    let data = response_to_json(&response).unwrap();
1169
1170    // The response should be a ServicesSdgs with an "items" map
1171    let items = data
1172        .get("items")
1173        .expect("response should contain 'items'")
1174        .as_object()
1175        .expect("items should be an object");
1176
1177    assert!(
1178        !items.is_empty(),
1179        "items map should contain at least one entry"
1180    );
1181
1182    // Find the entry - key format is "{service_name}_{action:?}" lowercased
1183    let entry = items
1184        .values()
1185        .next()
1186        .expect("should have at least one service SDG entry");
1187
1188    let sdgs = entry
1189        .get("sdgs")
1190        .expect("entry should have 'sdgs'")
1191        .as_array()
1192        .expect("sdgs should be an array");
1193
1194    assert!(!sdgs.is_empty(), "sdgs array should not be empty");
1195
1196    let sdg = sdgs
1197        .first()
1198        .expect("sdgs array should have at least one element");
1199    assert_eq!(
1200        sdg.get("caption").unwrap().as_str(),
1201        Some("flux_capacitor_sdg")
1202    );
1203    assert_eq!(sdg.get("si").unwrap().as_str(), Some("sensor_metadata"));
1204
1205    let inner = sdg
1206        .get("sdgs")
1207        .unwrap()
1208        .as_array()
1209        .expect("nested sdgs should be an array");
1210    assert_eq!(inner.len(), 1);
1211
1212    let sd = inner
1213        .first()
1214        .expect("inner sdgs should have at least one element");
1215    assert_eq!(sd.get("si").unwrap().as_str(), Some("measurement_unit"));
1216    assert_eq!(sd.get("value").unwrap().as_str(), Some("gigawatts"));
1217}
1218
[docs]1219/// [[ itest~sovd-api-component-operations-sdgsd, Operation-level SDG retrieval, itest ]]
1220#[tokio::test]
1221async fn test_operation_sdg_retrieval() {
1222    let (runtime, _lock) = setup_integration_test(true).await.unwrap();
1223    let auth = auth_header(&runtime.config, None).await.unwrap();
1224
1225    let params = QueryParams(HashMap::from_iter([(
1226        "x-sovd2uds-includesdgs".to_string(),
1227        "true".to_string(),
1228    )]));
1229    let response = send_cda_request(
1230        &runtime.config,
1231        &format!("{}/operations/SelfTest", sovd::ECU_FLXC1000_ENDPOINT),
1232        StatusCode::OK,
1233        Method::GET,
1234        None,
1235        Some(&auth),
1236        Some(&params),
1237    )
1238    .await
1239    .expect("Failed to get operation SDGs");
1240
1241    let data = response_to_json(&response).unwrap();
1242
1243    // The response should be a ServicesSdgs with an "items" map
1244    let items = data
1245        .get("items")
1246        .expect("response should contain 'items'")
1247        .as_object()
1248        .expect("items should be an object");
1249
1250    assert!(
1251        !items.is_empty(),
1252        "items map should contain at least one entry"
1253    );
1254
1255    // Find the entry
1256    let entry = items
1257        .values()
1258        .next()
1259        .expect("should have at least one service SDG entry");
1260
1261    let sdgs = entry
1262        .get("sdgs")
1263        .expect("entry should have 'sdgs'")
1264        .as_array()
1265        .expect("sdgs should be an array");
1266
1267    assert!(!sdgs.is_empty(), "sdgs array should not be empty");
1268
1269    let sdg = sdgs
1270        .first()
1271        .expect("sdgs array should have at least one element");
1272    assert_eq!(sdg.get("caption").unwrap().as_str(), Some("self_test_sdg"));
1273    assert_eq!(sdg.get("si").unwrap().as_str(), Some("routine_metadata"));
1274
1275    let inner = sdg
1276        .get("sdgs")
1277        .unwrap()
1278        .as_array()
1279        .expect("nested sdgs should be an array");
1280    assert_eq!(inner.len(), 1);
1281
1282    let sd = inner
1283        .first()
1284        .expect("inner sdgs should have at least one element");
1285    assert_eq!(sd.get("si").unwrap().as_str(), Some("expected_duration_ms"));
1286    assert_eq!(sd.get("value").unwrap().as_str(), Some("5000"));
1287}
1288
1289/// Polls until the ECU reaches the expected state, then asserts.
1290///
1291/// ECU states are updated asynchronously (variant detection tasks probe each
1292/// ECU on its transport, with per-probe timeouts), so a one-shot check races
1293/// the startup/detection loop - especially in mixed mode where undetected
1294/// CAN-mapped ECUs cost a probe timeout each before the loop moves on.
1295async fn validate_ecu_state(
1296    runtime: &TestRuntime,
1297    auth: &HeaderMap,
1298    ecu: &str,
1299    expected_state: sovd_interfaces::components::ecu::State,
1300) {
1301    let started = std::time::Instant::now();
1302    let mut status = ecu_status(&runtime.config, auth, ecu)
1303        .await
1304        .expect("failed to get ecu status");
1305    while status.variant.state != expected_state && started.elapsed() < Duration::from_secs(10) {
1306        cda_interfaces::util::tokio_ext::sleep_for(Duration::from_millis(200)).await;
1307        status = ecu_status(&runtime.config, auth, ecu)
1308            .await
1309            .expect("failed to get ecu status");
1310    }
1311    assert_eq!(
1312        status.variant.state, expected_state,
1313        "ECU {ecu} state does not match {status:?}"
1314    );
1315}
1316
1317async fn session(
1318    config: &Configuration,
1319    headers: &HeaderMap,
1320    ecu_endpoint: &str,
1321) -> Result<
1322    sovd_interfaces::components::ecu::modes::security_and_session::get::Response,
1323    TestingError,
1324> {
1325    get_mode(config, headers, ecu_endpoint, "session").await
1326}
1327
1328async fn security(
1329    config: &Configuration,
1330    headers: &HeaderMap,
1331    ecu_endpoint: &str,
1332) -> Result<
1333    sovd_interfaces::components::ecu::modes::security_and_session::get::Response,
1334    TestingError,
1335> {
1336    get_mode(config, headers, ecu_endpoint, "security").await
1337}
1338
1339pub(crate) async fn switch_session(
1340    name: &str,
1341    config: &Configuration,
1342    headers: &HeaderMap,
1343    ecu_endpoint: &str,
1344    expected_status: StatusCode,
1345) -> Result<
1346    Option<sovd_interfaces::components::ecu::modes::security_and_session::put::Response<String>>,
1347    TestingError,
1348> {
1349    put_mode(
1350        config,
1351        headers,
1352        ecu_endpoint,
1353        "session",
1354        sovd_interfaces::components::ecu::modes::security_and_session::put::Request {
1355            value: name.to_owned(),
1356            mode_expiration: None,
1357            key: None,
1358        },
1359        expected_status,
1360    )
1361    .await
1362}
1363
1364async fn request_seed(
1365    name: String,
1366    config: &Configuration,
1367    headers: &HeaderMap,
1368    ecu_endpoint: &str,
1369) -> Result<
1370    Option<sovd_interfaces::components::ecu::modes::security_and_session::put::RequestSeedResponse>,
1371    TestingError,
1372> {
1373    put_mode(
1374        config,
1375        headers,
1376        ecu_endpoint,
1377        "security",
1378        sovd_interfaces::components::ecu::modes::security_and_session::put::Request {
1379            value: name,
1380            mode_expiration: None,
1381            key: None,
1382        },
1383        StatusCode::OK,
1384    )
1385    .await
1386}
1387
1388async fn send_key(
1389    name: String,
1390    key: String,
1391    config: &Configuration,
1392    headers: &HeaderMap,
1393    ecu_endpoint: &str,
1394    excepted_status: StatusCode,
1395) -> Result<
1396    Option<sovd_interfaces::components::ecu::modes::security_and_session::put::Response<String>>,
1397    TestingError,
1398> {
1399    put_mode(
1400        config,
1401        headers,
1402        ecu_endpoint,
1403        "security",
1404        sovd_interfaces::components::ecu::modes::security_and_session::put::Request {
1405            value: name,
1406            mode_expiration: None,
1407            key: Some(
1408                sovd_interfaces::components::ecu::modes::security_and_session::put::ModeKey {
1409                    send_key: key,
1410                },
1411            ),
1412        },
1413        excepted_status,
1414    )
1415    .await
1416}
1417
1418async fn get_mode<T: DeserializeOwned>(
1419    config: &Configuration,
1420    headers: &HeaderMap,
1421    ecu_endpoint: &str,
1422    sub_path: &str,
1423) -> Result<T, TestingError> {
1424    let http_response = send_cda_request(
1425        config,
1426        &format!("{ecu_endpoint}/modes/{sub_path}"),
1427        StatusCode::OK,
1428        Method::GET,
1429        None,
1430        Some(headers),
1431        None,
1432    )
1433    .await?;
1434    response_to_t(&http_response)
1435}
1436
1437async fn ecu_status(
1438    config: &Configuration,
1439    headers: &HeaderMap,
1440    ecu_endpoint: &str,
1441) -> Result<sovd_interfaces::components::ecu::get::Response, TestingError> {
1442    let http_response = send_cda_request(
1443        config,
1444        ecu_endpoint,
1445        StatusCode::OK,
1446        Method::GET,
1447        None,
1448        Some(headers),
1449        None,
1450    )
1451    .await?;
1452    response_to_t(&http_response)
1453}
1454
1455async fn force_variant_detection(
1456    config: &Configuration,
1457    headers: &HeaderMap,
1458    ecu_endpoint: &str,
1459) -> Result<(), TestingError> {
1460    send_cda_request(
1461        config,
1462        ecu_endpoint,
1463        StatusCode::CREATED,
1464        Method::PUT,
1465        None,
1466        Some(headers),
1467        None,
1468    )
1469    .await?;
1470    Ok(())
1471}
1472
1473async fn get_comm_control(
1474    config: &Configuration,
1475    headers: &HeaderMap,
1476    ecu_endpoint: &str,
1477) -> Result<modes::commctrl::get::Response, TestingError> {
1478    get_mode(config, headers, ecu_endpoint, "commctrl").await
1479}
1480
1481async fn set_comm_control(
1482    value: &str,
1483    parameters: Option<cda_interfaces::HashMap<String, serde_json::Value>>,
1484    config: &Configuration,
1485    headers: &HeaderMap,
1486    ecu_endpoint: &str,
1487    expected_status: StatusCode,
1488) -> Result<Option<sovd_interfaces::components::ecu::modes::commctrl::put::Response>, TestingError>
1489{
1490    put_mode(
1491        config,
1492        headers,
1493        ecu_endpoint,
1494        "commctrl",
1495        modes::commctrl::put::Request {
1496            value: value.to_owned(),
1497            parameters,
1498        },
1499        expected_status,
1500    )
1501    .await
1502}
1503
1504pub(crate) async fn get_dtc_setting(
1505    config: &Configuration,
1506    headers: &HeaderMap,
1507    ecu_endpoint: &str,
1508) -> Result<dtcsetting::get::Response, TestingError> {
1509    get_mode(config, headers, ecu_endpoint, "dtcsetting").await
1510}
1511
1512async fn get_configurations(
1513    config: &Configuration,
1514    headers: &HeaderMap,
1515    ecu_endpoint: &str,
1516    service: &str,
1517) -> Result<sovd_interfaces::components::ecu::configurations::ServiceResponse, TestingError> {
1518    let http_response = send_cda_request(
1519        config,
1520        &format!("{ecu_endpoint}/configurations/{service}"),
1521        StatusCode::OK,
1522        Method::GET,
1523        None,
1524        Some(headers),
1525        None,
1526    )
1527    .await?;
1528    response_to_t(&http_response)
1529}
1530
1531async fn reset_ecu(
1532    value: &str,
1533    config: &Configuration,
1534    headers: &HeaderMap,
1535    ecu_endpoint: &str,
1536    expected_status: StatusCode,
1537) -> Result<(), TestingError> {
1538    let body = serde_json::json!({
1539        "parameters": {"value": value}
1540    })
1541    .to_string();
1542    send_cda_request(
1543        config,
1544        &format!("{ecu_endpoint}/operations/reset/executions"),
1545        expected_status,
1546        Method::POST,
1547        Some(&body),
1548        Some(headers),
1549        None,
1550    )
1551    .await?;
1552    Ok(())
1553}
1554
1555async fn get_data_services(
1556    config: &Configuration,
1557    headers: &HeaderMap,
1558    ecu_endpoint: &str,
1559) -> Result<sovd_interfaces::components::ecu::data::get::Response, TestingError> {
1560    let http_response = send_cda_request(
1561        config,
1562        &format!("{ecu_endpoint}/data"),
1563        StatusCode::OK,
1564        Method::GET,
1565        None,
1566        Some(headers),
1567        None,
1568    )
1569    .await?;
1570    response_to_t(&http_response)
1571}