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