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